Node-red-nodes-ui-table - Tabulator Version

Are there any plans to update the Tabulator library being used in the ui-table?
It's using 4.4.1, with the latest (as of writing) version 5.3.1.

I was looking to use a couple of options in 5.3

Happy to look at a tested pull request to upgrade it if someone can check the jump from 4 to 5 doesn’t break anything. Or to patch it if it does. Thanks

With very limited testing, I replaced the existing library files (/lib/css and /lib/js) with the new ones and have not seen any errors in the logs. Everything is working fine for what I need.

I couldn't see any tests, but I ran all the examples and they all worked without issue.

1 Like

OK - updated node (v0.4.0) to use latest libs.

2 Likes

The update does seem to have broken something for me, I'm using a subflow ui-table handler by @Christian-Me.

This was working OK before the update, but now the table is empty, and I see these messages in browser console -

@dceejay @Christian-Me Any thoughts why updating the ui table node broke this ?

Is there an easy way to roll back to the old version ?

Also tried this in firefox with the following result

Yes. Underlying library updated to fix vulnerabilities. Broke constructor.

0.4.1 fixed some of it, but can you share your example that seems to break more ?

Can install. @0.3 to get previous version

Just to confirm that all is good when I roll back to 3.0

Not sure what you need, its a big flow based on work by @Christian-Me

This is the subflow that creates the table -

[{"id":"2924702c.b33a7","type":"subflow","name":"ui-table handler","info":"# ui-table handler\nUniversal handler for ui-table.\n## features\n- buffer table data\n- add or update individual rows or cells of the table\n- delete rows\n- clear tableData\n- handle column width\n- handle column order\n\n## configuration\n- `tabulator` json formatted object containing configuration of the table. See ui-table for more details.\n- `property` property of the msg object that contains the data to be passed to ui-table. I.e. *state* `msg.state`\n- `index` index column to identify individual rows. Each message containing data must have a unique `msg.topic` to identify the row. Messages without this `msg.topic` will be droped. It is not nessesary but possible to display the index column in the table. Do not enable editing on this column otherwise you will loose the connection and another row will be added to the table as soon as a new message arrives!\n\n   Defaults to *$topic* `msg.state.$topic`\n- `dashboard` name of the dashboard tab to only update the table if the dashboard is visible. If empty the table will be updated on every tab change and connect.\n- `context` configuration of context data. The subflow will save or cache data in the flows context using `$parent.`. \n   **tableData** caches the incoming data to restore it on `ui-control´ *change* messages.\n   **tableConfig** saves column width and order to save the interactive table layot\n   **tableEdit** saves edits on the table data otherwise it would be overwritten when new data arrives\n```json\n{\n    \"tableData\": {\n        \"name\": \"tableData\"\n    },\n    \"tableConfig\": {\n        \"name\": \"tableConfig\",\n        \"storage\": \"file\"\n    },\n    \"tableEdit\": {\n        \"name\": \"tableEdit\",\n        \"storage\": \"file\"\n    }\n}\n```\n\n## commands\ncommands can be passed by sending a object as `msg.payload`\n- delete custom colum order and restore colum order from the tabulator json. This is important if you add or delete columns in the tabulator config otherwise the columns most likely don`t show up\n```json\n{\n    \"command\": \"delete\",\n    \"object\": \"columnOrder\"\n}\n```\n- delete custom column widths\n```json\n{\n    \"command\": \"delete\",\n    \"object\": \"columnWidth\"\n}\n```\n- delete tableCache\n```json\n{\n    \"command\": \"delete\",\n    \"object\": \"tableCache\"\n}\n```\n\n## background\nui-table warps the powerfull tabluator library. This subflow makes it easier to unleash the powerfull features of ui-table","category":"dashboard","in":[{"x":54,"y":85,"wires":[{"id":"5eb0bd6b.74b794"}]}],"out":[{"x":360,"y":85,"wires":[{"id":"5eb0bd6b.74b794","port":1}]},{"x":360,"y":136,"wires":[{"id":"5eb0bd6b.74b794","port":2}]}],"env":[{"name":"tabulator","type":"json","value":"{\"tabulator\":{\"responsiveLayout\":\"collapse\",\"responsiveLayoutCollapseStartOpen\":false,\"index\":\"$name\",\"layout\":\"fitColumns\",\"movableColumns\":true,\"groupBy\":\"\",\"columnResized\":\"function(column){     var newColumn = {         field: column._column.field,         visible: column._column.visible,         width: column._column.width,         widthFixed: column._column.widthFixed,         widthStyled: column._column.widthStyled     }; this.send({topic:this.config.topic,ui_control:{callback:'columnResized',columnWidths:newColumn}}); }\",\"columnMoved\":\"function(column, columns){     var newColumns=[];     columns.forEach(function (column) {         newColumns.push({'field': column._column.definition.field, 'title': column._column.definition.title});     });     this.send({topic:this.config.topic,ui_control:{callback:'columnMoved',columns:newColumns}}); }\",\"rowFormatter\":\"function(row){     var data = row.getData();     switch (data.$state) {         case \\\"lost\\\":             row.getElement().style.backgroundColor = \\\"#9e2e66\\\";             row.getElement().style.color = \\\"#a6a6a6\\\";             break;         case \\\"sleeping\\\":             row.getElement().style.backgroundColor = \\\"#336699\\\";             break;         case \\\"disconnected\\\":             row.getElement().style.backgroundColor = \\\"#cc3300\\\";             row.getElement().style.color = \\\"#a6a6a6\\\";             break;         case \\\"alert\\\":             row.getElement().style.backgroundColor = \\\"#A6A6DF\\\";             break;         case \\\"init\\\":             row.getElement().style.backgroundColor = \\\"#f2f20d\\\";             break;         case \\\"ready\\\":             row.getElement().style.backgroundColor = \\\"\\\";             row.getElement().style.color = \\\"\\\";             break;         } }\",\"columns\":[{\"formatter\":\"responsiveCollapse\",\"width\":30,\"minWidth\":30,\"align\":\"center\",\"resizable\":false,\"headerSort\":false,\"frozen\":true,\"title\":\"expand\",\"field\":\"expand\",\"headerVertical\":\"flip\"},{\"formatter\":\"function(cell, formatterParams, onRendered) {      var html = cell.getValue(); return html;  }\",\"title\":\"State\",\"field\":\"$stateIcon\",\"width\":100,\"frozen\":true,\"headerVertical\":\"flip\"},{\"formatter\":\"function(cell, formatterParams, onRendered) {     var html = cell.getValue(); return html;  }\",\"title\":\"Signal\",\"field\":\"signalIcon\",\"width\":100,\"frozen\":true,\"headerVertical\":\"flip\"},{\"title\":\"Name\",\"field\":\"$name\",\"width\":100,\"frozen\":true,\"headerVertical\":\"flip\"},{\"title\":\"State\",\"field\":\"$state\",\"width\":100,\"align\":\"center\",\"headerVertical\":\"flip\"},{\"title\":\"last-ready\",\"field\":\"lastSeenreadyFormatted\",\"width\":100,\"align\":\"left\",\"headerVertical\":\"flip\"},{\"title\":\"Homie\",\"field\":\"$homie\",\"width\":100,\"align\":\"left\",\"headerVertical\":\"flip\"},{\"title\":\"Platform\",\"field\":\"$implementation\",\"width\":100,\"align\":\"left\",\"headerVertical\":\"flip\"},{\"title\":\"Statistics\",\"columns\":[{\"title\":\"Interval\",\"field\":\"interval\",\"width\":100,\"headerVertical\":\"flip\"},{\"formatterParams\":{\"outputFormat\":\"d hh:mm:ss\",\"inputFormat\":\"seconds\",\"invalidPlaceholder\":\"(unknown)\"},\"title\":\"Uptime\",\"field\":\"uptime\",\"formatter\":\"function(cell, formatterParams, onRendered){     var pad = function (num) {         return (\\\"0\\\"+num).slice(-2);     };     var secs = Number(cell.getValue());     if (Number.isNaN(secs)) return;     var minutes = Math.floor(secs / 60);     secs = secs%60;     var hours = Math.floor(minutes/60);     minutes = minutes%60;     var days = Math.floor(hours/24);     hours = hours%24;     if (days>0)         return days+\\\"d \\\"+pad(hours)+\\\":\\\"+pad(minutes);     else         return pad(hours)+\\\":\\\"+pad(minutes)+\\\":\\\"+pad(secs); }\",\"width\":100,\"headerVertical\":\"flip\"},{\"formatterParams\":{\"min\":0,\"max\":100,\"color\":[\"red\",\"orange\",\"green\"],\"legend\":\"function (value) {if (value>0) return \\\"<span style='color:#FFFFFF;'>\\\"+value+\\\" %</span>\\\"; else return; }\",\"legendColor\":\"#FFFFFF\",\"legendAlign\":\"center\"},\"title\":\"Signal\",\"field\":\"signal\",\"formatter\":\"progress\",\"width\":100,\"headerVertical\":\"flip\"},{\"formatterParams\":{\"min\":2.5,\"max\":3.5,\"color\":[\"red\",\"green\",\"red\"],\"legend\":\"function (value) { if (value>0) return \\\"<span style='color:#FFFFFF;'>\\\"+value+\\\" V</span>\\\"; else return; }\",\"legendColor\":\"#101010\",\"legendAlign\":\"center\"},\"title\":\"Supply\",\"field\":\"supply\",\"formatter\":\"progress\",\"width\":100,\"headerVertical\":\"flip\"},{\"formatterParams\":{\"min\":0,\"max\":100,\"color\":[\"red\",\"orange\",\"green\"],\"legend\":\"function (value) {     if (value>0)         return \\\"<span style='color:#FFFFFF;'>\\\"+value+\\\" %</span>\\\";     else         return; }\",\"legendColor\":\"#101010\",\"legendAlign\":\"center\"},\"title\":\"Battery\",\"field\":\"battery\",\"formatter\":\"progress\",\"width\":100,\"headerVertical\":\"flip\"},{\"formatterParams\":{\"min\":0,\"max\":100000,\"color\":[\"red\",\"orange\",\"green\"],\"legend\":\"function (value) { if (value>0) return \\\"<span style='color:#FFFFFF;'>\\\"+(value/1024).toFixed(2)+\\\" kB</span>\\\"; else return; }\",\"legendColor\":\"#101010\",\"legendAlign\":\"center\"},\"title\":\"Memory\",\"field\":\"freeheap\",\"formatter\":\"progress\",\"width\":100,\"headerVertical\":\"flip\"},{\"formatterParams\":{\"target\":\"_blank\",\"min\":0,\"max\":100,\"color\":[\"red\",\"orange\",\"green\"],\"legend\":\"function (value) {     if (value>0)         return \\\"<span style='color:#FFFFFF;'>\\\"+value+\\\" %</span>\\\";     else         return; }\",\"legendColor\":\"#101010\",\"legendAlign\":\"center\"},\"title\":\"CPU load\",\"field\":\"cpuload\",\"formatter\":\"progress\",\"width\":100,\"headerVertical\":\"flip\"},{\"formatterParams\":{\"min\":20,\"max\":60,\"color\":[\"green\",\"orange\",\"red\"],\"legend\":\"function (value) { if (value>0) return \\\"<span style='color:#FFFFFF;'>\\\"+value+\\\" °C</span>\\\"; else return; }\",\"legendColor\":\"#101010\",\"legendAlign\":\"center\"},\"title\":\"CPU temp\",\"field\":\"cputemp\",\"formatter\":\"progress\",\"width\":100,\"headerVertical\":\"flip\"}]},{\"title\":\"Firmware\",\"columns\":[{\"formatter\":\"link\",\"formatterParams\":{\"labelField\":\"$localip\",\"urlPrefix\":\"http://\",\"target\":\"_blank\"},\"title\":\"IP\",\"field\":\"$localip\",\"width\":100},{\"title\":\"mac\",\"field\":\"$mac\",\"width\":100},{\"title\":\"Accsess Point\",\"field\":\"SSID\",\"width\":100},{\"title\":\"Firmware\",\"field\":\"name\",\"width\":100},{\"title\":\"Version\",\"field\":\"version\",\"width\":100},{\"title\":\"Last Boot Cause\",\"field\":\"lastBootCause\",\"width\":100},{\"title\":\"Reset Reason\",\"field\":\"resetReason\",\"width\":100}]}]},\"customHeight\":12}","ui":{"icon":"font-awesome/fa-table","label":{"en-US":"Tabulator"},"type":"input","opts":{"types":["json","env"]}}},{"name":"tableDataProp","type":"str","value":"row","ui":{"icon":"font-awesome/fa-tag","label":{"en-US":"rowProperty"}}},{"name":"tableIndex","type":"str","value":"$topic","ui":{"icon":"font-awesome/fa-indent","label":{"en-US":"Index"},"type":"input","opts":{"types":["str","json","env"]}}},{"name":"dashboard","type":"str","value":"Remote Device Table","ui":{"icon":"font-awesome/fa-dashboard","label":{"en-US":"Dashboard"},"type":"input","opts":{"types":["str","env"]}}},{"name":"tableContext","type":"json","value":"{\"tableData\":{\"name\":\"tableData\"},\"tableConfig\":{\"name\":\"tableConfig\"},\"tableEdit\":{\"name\":\"tableEdit\"}}","ui":{"icon":"font-awesome/fa-database","label":{"en-US":"Context"},"type":"input","opts":{"types":["json","env"]}}}],"meta":{},"color":"#3FADB5","icon":"node-red-dashboard/ui_slider.png","status":{"x":360,"y":34,"wires":[{"id":"5eb0bd6b.74b794","port":0}]}},{"id":"5eb0bd6b.74b794","type":"function","z":"2924702c.b33a7","name":"handle tableData","func":"var status = {fill:\"red\",shape:\"dot\",text: \"payload \"};\nvar tableIndex = env.get(\"tableIndex\") || \"$topic\";\nvar tableDataProp = env.get(\"tableDataProp\") || \"row\";\nvar tableContext = env.get(\"tableContext\");\nvar dashboard = env.get(\"dashboard\");\nif (!tableContext.hasOwnProperty(\"tableData\") || !tableContext.hasOwnProperty(\"tableConfig\")) {\n    status.text=\"tableContext not defined\";\n    node.error(status.text);\n    return [{payload:status},null];\n}\n\n// context store to cache table data (memoryOnly prefered)\nvar tableData = flow.get(\"$parent.\"+tableContext.tableData.name,tableContext.tableData.storage);\nif (tableData===undefined) {\n    node.warn(\"[ui-table handler] tableData initialized!\");\n    tableData={};\n    flow.set(\"$parent.\"+tableContext.tableData.name,tableData,tableContext.tableData.storage);\n}\n\n// context Store to save table configuration (file)\nvar tableConfig = flow.get(\"$parent.\"+tableContext.tableConfig.name,tableContext.tableConfig.storage);\nif (tableConfig===undefined) {\n    node.warn(\"[ui-table handler] tableConfig initialized!\");\n    tableConfig={ResponsiveLayout:true};\n    flow.set(\"$parent.\"+tableContext.tableConfig.name,tableConfig,tableContext.tableConfig.storage);\n}\n\n// context Store to save table configuration (file)\nvar tableEdit;\nif (tableContext.hasOwnProperty(\"tableEdit\")) {\n    tableEdit = flow.get(\"$parent.\"+tableContext.tableEdit.name,tableContext.tableEdit.storage);\n    if (tableEdit===undefined) {\n        node.warn(\"[ui-table handler] tableEdit initialized!\");\n        tableEdit={};\n        flow.set(\"$parent.\"+tableContext.tableEdit.name,tableEdit,tableContext.tableEdit.storage);\n    }\n}\n\n// function to merge partial data into existing table row\nvar mergeObject = function (destination, source, filter) {\n    for (let currentSource in source) {\n        if (source.hasOwnProperty(currentSource)) {\n            if (filter!==undefined && tableEdit && tableEdit.hasOwnProperty(filter) && tableEdit[filter].hasOwnProperty(currentSource)) {\n                destination[currentSource]= tableEdit[filter][currentSource];\n                source[currentSource]=tableEdit[filter][currentSource];\n            } else {\n                destination[currentSource]= source[currentSource];\n            }\n        }    \n    }\n    return source;\n};\n    \n// deep search for a column\nvar searchTabulatorColumn = function (columns,key,match) {\n    var result;\n    for (let column of columns) {\n        if (column.hasOwnProperty(\"columns\")) {\n            result = searchTabulatorColumn(column.columns,key,match);\n            if (result!==undefined) return result;\n        } else if (column.hasOwnProperty(key) && column[key]===match) {\n            return column;\n        }\n    }\n};\n\n// command message to update add or update data on ui-table\nvar msgToTable={};\nmsgToTable.payload={\n    \"command\":\"updateOrAddData\",\n    \"arguments\": [],\n    \"returnPromise\": false\n};\n\nif (msg.hasOwnProperty(tableDataProp)) {\n    // store data for later recover\n    if (!msg.hasOwnProperty(\"topic\")) { // check if index existst\n        status.text=\"msg.topic not defined!\";\n        return [{payload:status},null];\n    }\n    if (!tableData.hasOwnProperty(msg.topic)){ // first seen\n        tableData[msg.topic]={};\n        if (tableEdit && tableEdit.hasOwnProperty(msg.topic)) { // table edits available!\n            Object.keys(tableEdit[msg.topic]).forEach((key) => {\n                msg[tableDataProp][key]=tableEdit[msg.topic][key];\n                tableData[msg.topic][key]=tableEdit[msg.topic][key];\n            })\n        }\n    }\n    if (!tableData[msg.topic].hasOwnProperty(tableIndex)) tableData[msg.topic][tableIndex]=msg.topic;\n    msg[tableDataProp]=mergeObject(tableData[msg.topic],msg[tableDataProp],msg.topic);\n    msg[tableDataProp][tableIndex]=msg.topic;\n    msgToTable.payload.arguments=[[msg[tableDataProp]]];\n    status.fill=\"green\";\n    status.text=msg.topic+\" updated\";\n    return [{payload:status},msgToTable,null];\n} //if (msg.payload===\"connect\" || (msg.payload===\"change\" && msg.name===dashboard) || (msg.hasOwnProperty(\"payload\") && msg.payload.hasOwnProperty(\"command\"))) { \n    if (msg.payload === \"connect\" || (msg.payload === \"change\" && msg.name === \"Remote Device Table\") || (msg.hasOwnProperty(\"payload\") && msg.payload.hasOwnProperty(\"command\"))) {\n    if (!msg.hasOwnProperty(\"ui_control\")) {\n        msg.ui_control = env.get('tabulator');\n        status.text+=\" ui_control added\";\n    }\n    //process commands\n    //node.warn({\"command\":msg.payload.command,\"msg\":msg,\"object\":msg.payload.object})\n    if (msg.payload.hasOwnProperty(\"command\")) {\n        status.fill=\"blue\";\n        switch(msg.payload.command) {\n            case 'deleteTable':\n                flow.set(\"$parent.\"+tableContext.tableData.name,undefined,tableContext.tableData.storage);\n                tableData={};\n                status.text=\"tabledata deleted\";\n                node.warn(\"[ui-table handler] \"+\"tabledata deleted\");\n                break;\n            case 'deleteDevice':\n                if (tableData.hasOwnProperty(msg.payload.object)) {\n                    delete tableData[msg.payload.object];\n                    status.text=msg.payload.object+\" deleted\";\n                    if (tableEdit.hasOwnProperty(msg.payload.object)) { // added by sean\n                    delete tableEdit[msg.payload.object]}\n                } else {\n                    status.fill=\"yellow\";\n                    status.text=msg.payload.object+\" undefined\";\n                }\n                break;\n            case 'ignoreDevice':\n                if (tableData.hasOwnProperty(msg.payload.object)) {\n                    delete tableData[msg.payload.object];\n                    status.text=msg.payload.object+\" will be ignored\";\n                    if (!tableConfig.hasOwnProperty('ignoreDevice')) tableConfig.ignoreDevice={};\n                    tableConfig.ignoreDevice[msg.payload.object]=true;\n                }\n                break;\n            case 'unIgnoreDevice':\n                if (tableConfig.hasOwnProperty('ignoreDevice')) {\n                    delete tableConfig.ignoreDevice[msg.payload.object];\n                }\n                break;\n            case 'unIgnoreDevices':\n                delete tableConfig.ignoreDevice;\n                break;\n            case 'updateData':\n                status.text=\"column \"+msg.payload.column+\" updated\";\n                return [{payload:status},msg];\n            case 'updateTable':\n                status.text=msg.payload.command+\": \";\n                break;\n            case 'columnHide':\n                if (!tableConfig.hasOwnProperty('columnVisible')) tableConfig.columnVisible={};\n                tableConfig.columnVisible[msg.payload.object]=false;\n                break;\n            case 'columnUnHide':\n                if (!tableConfig.hasOwnProperty('columnVisible')) tableConfig.columnVisible={};\n                tableConfig.columnVisible[msg.payload.object]=true;\n                break;\n            case 'columnsUnHide':\n                for (let column in tableConfig.columnVisible) {\n                    if (tableConfig.columnVisible.hasOwnProperty(column)) tableConfig.columnVisible[column]=true;\n                }\n                break;\n            case 'refreshTable':\n                break;\n            case 'restoreColumnOrder':\n                delete tableConfig.columns;\n                break;\n            case 'resetColumnWidth':\n                delete tableConfig.columnWidths;\n                break;\n            case 'setResponsiveLayout':\n                tableConfig.ResponsiveLayout=!tableConfig.ResponsiveLayout;\n                break;\n            default:\n                status.fill=\"red\";\n                status.text=\"unknown command \"+msg.payload.command;\n                node.warn(\"[ui-table handler] \"+status.text);\n                break;\n        }\n        flow.set(\"$parent.\"+tableContext.tableConfig.name,tableConfig,tableContext.tableConfig.storage);\n        node.send([{payload:status},null,null]);\n    }\n\n    // crawl through tabulator arrays and updated user defined values\n    var crawlTabulator = function (columns,match,config,property) {\n        for (let column of columns) {\n            if (column.hasOwnProperty(\"columns\")) {\n                crawlTabulator(column.columns,match,config,property);\n            } else if (config.hasOwnProperty(column[match])) column[property]=config[column.field];\n        }\n    };\n    \n    // restore custom column width\n    if (tableConfig.hasOwnProperty(\"columnWidths\") && msg.hasOwnProperty(\"ui_control\")) {\n        crawlTabulator(msg.ui_control.tabulator.columns,\"field\",tableConfig.columnWidths,\"width\");\n    }\n    \n    // restore custom column hide/show\n    if (tableConfig.hasOwnProperty(\"columnVisible\") && msg.hasOwnProperty(\"ui_control\")) {\n        crawlTabulator(msg.ui_control.tabulator.columns,\"field\",tableConfig.columnVisible,\"visible\");\n    }\n    \n    // restore custom responsive / standard view\n    if (tableConfig.hasOwnProperty(\"ResponsiveLayout\")) {\n        if (!tableConfig.ResponsiveLayout) {\n            msg.ui_control.tabulator.responsiveLayout=false;\n        }\n        msg.ui_control.tabulator.columns.forEach((column,index) => {\n            if (column.formatter===\"responsiveCollapse\") { // hide expand column on any position\n                column.visible=tableConfig.ResponsiveLayout;\n                return;\n            }\n        });\n    }\n\n    // sort columns\n    if (tableConfig.hasOwnProperty(\"columns\") && msg.hasOwnProperty(\"ui_control\") && msg.ui_control.hasOwnProperty(\"tabulator\")) {\n        var addedColumns = 0;\n        var sortColumnsByLayout = function (sortColumns, columnsLayout, targetColumns) {\n            for (var layoutColumn=0;  layoutColumn<columnsLayout.length; layoutColumn++) {\n                for (var sortColumn in sortColumns) {\n                    if (sortColumns[sortColumn].hasOwnProperty(\"columns\")) {\n                        targetColumns.push({\"title\":sortColumns[sortColumn].title, \"columns\":[]});\n                        sortColumnsByLayout(sortColumns[sortColumn].columns,columnsLayout,targetColumns[targetColumns.length-1].columns);\n                        layoutColumn=addedColumns; // jump forward after childes added\n                    } else {\n                        if (columnsLayout[layoutColumn].field===sortColumns[sortColumn].field){\n                            targetColumns.push(sortColumns[sortColumn]);\n                            addedColumns++;\n                            break;\n                        }\n                    }\n                }\n            }\n        };                 \n        var newColumns=[];\n        sortColumnsByLayout(msg.ui_control.tabulator.columns,tableConfig.columns,newColumns);\n        msg.ui_control.tabulator.columns=newColumns;\n    }\n\n    // restore stored lines after connect\n    msg.payload=[];\n    for (let device in tableData) {\n        if (tableConfig && tableConfig.hasOwnProperty(\"ignoreDevice\") && tableConfig.ignoreDevice[device]) {\n            continue;\n        }\n        // merge edits into table\n        if (tableEdit && tableEdit.hasOwnProperty(device)) {\n            let tableRow = RED.util.cloneMessage(tableData[device]);\n            Object.keys(tableEdit[device]).forEach((field) => {\n                tableRow[field]=tableEdit[device][field];\n            });\n            msg.payload.push(tableRow);\n        } else {\n            msg.payload.push(tableData[device]);\n        }\n    }\n    \n    status.fill=\"blue\";\n    status.text+=\" \"+msg.payload.length+\" rows restored\";\n    return [{payload:status},msg];\n} if (msg.hasOwnProperty(\"ui_control\")) {\n    // callback from tabulator\n    status.fill=\"blue\";\n    status.text=\"unknown callback \"+msg.ui_control.callback;\n    switch(msg.ui_control.callback) {\n        case \"columnResized\": // save new column width\n            if (tableConfig.columnWidths===undefined) tableConfig.columnWidths={};\n            tableConfig.columnWidths[msg.ui_control.columnWidths.field]=msg.ui_control.columnWidths.width;\n            flow.set(\"$parent.\"+tableContext.tableConfig.name,tableConfig,tableContext.tableConfig.storage);\n            status.text=msg.ui_control.columnWidths.field+\"=\"+msg.ui_control.columnWidths.width+\"px\";\n            break;\n        case \"columnMoved\": // save new column order\n            if (tableConfig.columns===undefined) tableConfig.columns=[];\n            tableConfig.columns=msg.ui_control.columns;\n            flow.set(\"$parent.\"+tableContext.tableConfig.name,tableConfig,tableContext.tableConfig.storage);\n            status.text=\"new column order\";\n            break;\n        case \"cellEdited\":\n            if (tableEdit) {\n                if (!tableEdit.hasOwnProperty(msg[tableIndex])) tableEdit[msg[tableIndex]]={};\n                tableEdit[msg[tableIndex]][msg.field] = msg.payload; // save data and mark es edited field\n                flow.set(\"$parent.\"+tableContext.tableEdit.name,tableEdit,tableContext.tableEdit.storage);\n                status.text=msg[tableIndex]+\" \"+msg.field+\" edited to \"+msg.payload;\n                msg[tableDataProp]={};\n                msg[tableDataProp][tableIndex]=msg[tableIndex];\n                msg[tableDataProp][msg.field]=msg.payload;\n                msgToTable.payload.arguments=[[msg[tableDataProp]]];\n                node.send([{payload:status},msgToTable,msg]);\n            } else {\n                node.error(\"[ui-table handler] no tableEdit store defined!\")\n            }\n            break;\n        case \"rowContext\":\n            msg.ignoredDevices=[];\n            for (let device in tableConfig.ignoreDevice) {\n                if (tableConfig.ignoreDevice.hasOwnProperty(device)) {\n                    msg.ignoredDevices.push({\"text\":device,\"icon\":\"fa fa-plug\",\"topic\":\"unIgnoreDevice\",\"payload\":device})  \n                }\n            }\n            break;\n        case \"headerContext\":\n            msg.hiddenColumns=[];\n            let tabulatorConfig = env.get('tabulator');\n            for (let column in tableConfig.columnVisible) {\n                if (tableConfig.columnVisible.hasOwnProperty(column) &&\n                    !tableConfig.columnVisible[column]) {\n                    let configColumn=searchTabulatorColumn(tabulatorConfig.tabulator.columns,\"field\",column);\n                    let icon;\n                    if (configColumn.hasOwnProperty('title') && configColumn.title.toLowerCase().includes('</i>')) {\n                        // <i class='fa fa-star-half-o'></i> State\n                        let start=configColumn.title.indexOf(\"'fa \");\n                        let end=configColumn.title.indexOf(\"'\",start+1);\n                        icon=configColumn.title.substring(start+4,end);\n                    }\n                    msg.hiddenColumns.push({\"text\":column,\"icon\":icon,\"topic\":\"columnUnHide\",\"payload\":configColumn.field})  \n                }\n            }\n            break;\n        default:\n            // if rowIndex exists pass complete object\n            if (msg.hasOwnProperty(tableIndex)) {\n                msg.rowData=tableData[msg[tableIndex]];\n            }\n            status.text=\"pass message\";\n    }\n    return [{payload:status},null,msg];\n}\n// nothing to do bejond this point\nstatus.text+=\" [\"+msg.payload+\"]\";\nreturn [{payload:status},null];\n","outputs":3,"noerr":0,"initialize":"","finalize":"","libs":[],"x":192,"y":85,"wires":[[],[],[]],"icon":"font-awesome/fa-table"}]

This is the table configuration -

{
    "customHeight": 26,
    "tabulator": {
        "responsiveLayout": "collapse",
        "responsiveLayoutCollapseStartOpen": false,
        "index": "$topic",
        "layout": "fitColumns",
        "movableColumns": true,
        "initialSort": [
            {
                "column": "$stateIcon",
                "dir": "asc"
            }
        ],
        "groupBy": "",
        "columns": [
            {
                "formatter": "responsiveCollapse",
                "width": 30,
                "minWidth": 30,
                "align": "center",
                "resizable": false,
                "headerSort": false,
                "frozen": true,
                "title": "Expand&nbsp;&nbsp;<i class='fa fa-plus-circle'></i> ",
                "field": "expand",
                "headerVertical": "flip",
                "headerTooltip": "click to expand more details",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "formatter": "function(cell, formatterParams, onRendered) {      var html = cell.getValue(); return html;  }",
                "title": "State&nbsp;&nbsp;<i class='fa fa-bolt fa-rotate-90'></i>",
                "field": "$stateIcon",
                "headerFilter": true,
                "width": 40,
                "align": "center",
                "frozen": true,
                "headerVertical": "flip",
                "headerTooltip": "current state as icon: ready, lost",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "formatter": "function(cell, formatterParams, onRendered) {     var html = cell.getValue(); return html;  }",
                "title": "Type&nbsp;&nbsp;<i class='fa fa-cogs'></i>",
                "field": "deviceTypeIcon",
                "width": 40,
                "align": "center",
                "frozen": true,
                "headerVertical": "flip",
                "headerTooltip": "device type",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "Name&nbsp;&nbsp;<i class='fa fa-tag fa-rotate-90'></i>",
                "field": "$name",
                "width": 140,
                "frozen": true,
                "tooltip": true,
                "headerVertical": "flip",
                "headerTooltip": "name of the device ($name)",
                "editor": "input",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContextNoHide'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "last seen&nbsp;&nbsp;<i class='fa fa-clock-o fa-rotate-90'></i>",
                "field": "lastSeenreadyFormatted",
                "sorter": "number",
                "width": 80,
                "align": "right",
                "tooltip": true,
                "headerVertical": "flip",
                "headerTooltip": "Time since the device last sent an update",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "Location&nbsp;&nbsp;<i class='fa fa-map-marker fa-rotate-90'></i>",
                "field": "room",
                "width": 100,
                "headerTooltip": "location of the device (room)",
                "tooltip": true,
                "headerVertical": "flip",
                "editor": "autocomplete",
                "editorParams": {
                    "freetext": true,
                    "allowEmpty": true,
                    "showListOnEmpty": true,
                    "values": true
                },
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "formatterParams": {
                    "allowEmpty": true,
                    "allowTruthy": true,
                    "sorter": "boolean",
                    "tickElement": "<i class='fa fa-check'style='color:#00cc00'></i>",
                    "crossElement": "<i class='fa fa-times'style='color:#cc0000'></i>"
                },
                "title": "monitor&nbsp;&nbsp;<i class='fa fa-undo'></i>",
                "field": "monitor",
                "editor": true,
                "formatter": "tickCross",
                "align": "center",
                "width": 30,
                "headerVertical": "flip",
                "headerTooltip": "Alert when lost",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "formatterParams": {
                    "outputFormat": "d hh:mm:ss",
                    "inputFormat": "seconds",
                    "invalidPlaceholder": "(unknown)"
                },
                "title": "uptime&nbsp;&nbsp;<i class='fa fa-clock-o'></i>",
                "field": "uptime",
                "formatter": "function(cell, formatterParams, onRendered){ var pad = function (num) { return (\"0\"+num).slice(-2); }; var secs = Number(cell.getValue()); if (Number.isNaN(secs)) return; var minutes = Math.floor(secs / 60); secs = secs%60; var hours = Math.floor(minutes/60); minutes = minutes%60; var days = Math.floor(hours/24); hours = hours%24; if (days>0) return days+\"d \"+pad(hours)+\":\"+pad(minutes); else return pad(hours)+\":\"+pad(minutes)+\":\"+pad(secs); }",
                "sorter": "number",
                "width": 75,
                "headerVertical": "flip",
                "headerTooltip": "device uptime",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "formatterParams": {
                    "min": 0,
                    "max": 100,
                    "color": [
                        "red",
                        "orange",
                        "green"
                    ],
                    "legend": "function (value) {if (value>0) return \"<span style='color:#FFFFFF;'>\"+value+\" %</span>\"; else return; }",
                    "legendColor": "#FFFFFF",
                    "legendAlign": "center"
                },
                "title": "signal&nbsp;&nbsp;<i class='fa fa-wifi'></i>",
                "field": "signal",
                "formatter": "progress",
                "sorter": "number",
                "width": 70,
                "headerVertical": "flip",
                "headerTooltip": "device wireless signal quality",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "SSID&nbsp;&nbsp;<i class='fa fa-wifi'></i>",
                "field": "SSID",
                "width": 100,
                "headerVertical": "flip",
                "headerTooltip": "access point the device device is connected to",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "Link&nbsp;&nbsp;<i class='fa fa-link'></i>",
                "field": "link",
                "width": 30,
                "sorter": "number",
                "headerVertical": "flip",
                "headerTooltip": "WiFi link Count",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "MQTT&nbsp;&nbsp;<i class='fa fa-link'></i>",
                "field": "mqtt",
                "width": 30,
                "sorter": "number",
                "headerVertical": "flip",
                "headerTooltip": "MQTT link count",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "boot&nbsp;&nbsp;<i class='fa  fa-exclamation-triangle fa-rotate-180'></i>",
                "field": "bootCount",
                "width": 30,
                "headerVertical": "flip",
                "headerTooltip": "Boot count",
                "sorter": "number",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "formatterParams": {
                    "min": 0,
                    "max": 100,
                    "color": [
                        "red",
                        "orange",
                        "green"
                    ],
                    "legend": "function (value) {     if (value>0)         return \"<span style='color:#FFFFFF;'>\"+value+\" %</span>\";     else         return; }",
                    "legendColor": "#101010",
                    "legendAlign": "center"
                },
                "title": "battery&nbsp;&nbsp;<i class='fa fa-battery-2'></i>",
                "field": "battery",
                "formatter": "progress",
                "width": 70,
                "headerVertical": "flip",
                "headerTooltip": "device battery level in %",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "formatterParams": {
                    "min": 0,
                    "max": 40000,
                    "color": [
                        "red",
                        "orange",
                        "green"
                    ],
                    "legend": "function (value) { if (value>0) return \"<span style='color:#FFFFFF;'>\"+(value/1024).toFixed(2)+\" kB</span>\"; else return; }",
                    "legendColor": "#101010",
                    "legendAlign": "center"
                },
                "title": "memory&nbsp;&nbsp;<i class='fa fa-microchip'></i>",
                "field": "freeheap",
                "formatter": "progress",
                "width": 70,
                "headerVertical": "flip",
                "headerTooltip": "device free heap memory in kb",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "formatterParams": {
                    "min": 0,
                    "max": 100,
                    "color": [
                        "green",
                        "orange",
                        "red"
                    ],
                    "legend": "function (value) {     if (value>0)         return \"<span style='color:#FFFFFF;'>\"+value+\" %</span>\";     else         return; }",
                    "legendColor": "#101010",
                    "legendAlign": "right"
                },
                "title": "load&nbsp;&nbsp;<i class='fa fa-tasks'></i>",
                "field": "cpuload",
                "formatter": "progress",
                "width": 70,
                "headerVertical": "flip",
                "headerTooltip": "device cpu load in %",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "formatter": "link",
                "formatterParams": {
                    "labelField": "$localip",
                    "urlPrefix": "http://",
                    "target": "_blank"
                },
                "title": "ip&nbsp;&nbsp;<i class='fa fa-globe'></i>",
                "field": "$localip",
                "width": 100,
                "headerVertical": "flip",
                "headerTooltip": "device local IP-Address ($localip)",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "version&nbsp;&nbsp;<i class='fa fa-code-fork'></i>",
                "field": "version",
                "width": 100,
                "headerVertical": "flip",
                "headerTooltip": "version of the firmware ($fw/version)",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "start time&nbsp;&nbsp;<i class='fa fa-clock-o '></i>",
                "field": "startTime",
                "width": 131,
                "align": "left",
                "headerVertical": "flip",
                "headerTooltip": "Startup time",
                "tooltip": true,
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "reset&nbsp;&nbsp;<i class='fa fa-power-off'></i>",
                "field": "resetReason",
                "width": 148,
                "headerVertical": "flip",
                "headerTooltip": "last reason of reset (provided by http json request)",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            }
        ],
        "columnResized": "function(column){     var newColumn = {         field: column._column.field,         visible: column._column.visible,         width: column._column.width,         widthFixed: column._column.widthFixed,         widthStyled: column._column.widthStyled     }; this.send({topic:this.config.topic,ui_control:{callback:'columnResized',columnWidths:newColumn}}); }",
        "columnMoved": "function(column, columns){     var newColumns=[];     columns.forEach(function (column) {         newColumns.push({'field': column._column.definition.field, 'title': column._column.definition.title});     });     this.send({topic:this.config.topic,ui_control:{callback:'columnMoved',columns:newColumns}}); }",
        "rowFormatter": "function(row){     var data = row.getData();     switch (data.$state) {         case \"lost\":             row.getElement().style.backgroundColor = \"#9e4a53\";             row.getElement().style.color = \"#a6a6a6\";             break;         case \"sleeping\":             row.getElement().style.backgroundColor = \"#336699\";             break;         case \"disconnected\":             row.getElement().style.backgroundColor = \"#cc3300\";             row.getElement().style.color = \"#a6a6a6\";             break;         case \"alert\":             row.getElement().style.backgroundColor = \"#A6A6DF\";             break;         case \"init\":             row.getElement().style.backgroundColor = \"#f2f20d\";             break;         case \"ready\":             row.getElement().style.backgroundColor = \"\";             row.getElement().style.color = \"\";             break;         } }",
        "rowContext": "function(e, row){     this.send({ui_control:{callback:'rowContext'},position:{\"x\":e.x,\"y\":e.y},payload:{\"$name\":row._row.data.$name,\"$localip\":row._row.data.$localip,\"name\":row._row.data.name},\"topic\":row._row.data.$topic});     e.preventDefault(); }",
        "cellEdited": "function(cell){     this.send({ui_control:{callback:'cellEdited'},         payload:cell._cell.value,         \"oldValue\":cell._cell.oldValue,         \"field\":cell._cell.column.field,         \"$topic\":cell._cell.row.data.$topic}); }"
    }
}

I have reverted (republished) the older 0.3.x as 0.4.2 (complete with old vulnerabilities) and moved the latest library version to a new branch while we work on it to make it more compatible.

1 Like