Skip to content

Instantly share code, notes, and snippets.

@Christian-Me
Last active March 14, 2023 20:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Christian-Me/35d0480ce9151b2a722fa9d185a37825 to your computer and use it in GitHub Desktop.
Save Christian-Me/35d0480ce9151b2a722fa9d185a37825 to your computer and use it in GitHub Desktop.
UI-Table Handler - Easy data handling for UI-Table

ui-table handler (subflow)

Handler for ui-table.

features

  • buffer table data (with limits)
  • add or update individual rows or cells of the table
  • delete rows
  • clear tableData
  • handle column width
  • handle column order
  • hide und unhide columns
  • hide and unhide rows
  • records and playback row order
  • table edits
  • export data
  • [...]
[{"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- hide und unhide columns\n- hide and unhide rows\n- records row order\n- support for nested columns [(column groups)](http://tabulator.info/examples/4.7#column-groups)\n- support for child rows (_children) [(nested data trees)](http://tabulator.info/examples/4.7#tree)\n\nFor real life example see:\n\n**syslog server** for logfile like table with filters\n\n**remote device table** for dynamically updated table with context menues\n\n**irrigation system** for sortable rows\n\n## sending data to ui-tabel\n\n- sending an `array` as discribed in ui-table will replace the complete table and delete all table edits\n \n if `msg.keepEdits=true` is added the existing edits will be kept.\n- send an `object` containing the updated properties of a table row by sending msg.<tableDataProp>.\n\n The table is updated using the `updateOrAddData` command. You can alter the command used by adding the `msg.tabulatorCommand` and `msg.tabulatorParameter`\n\n```\nmsg.tabulatorCommand=\"addData\";\nmsg.tabulatorParameter=[true];\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- `maxRows`maximum number of rows held by table widget. If grater than **0** the amount of rows in ui-table is limited. For this to work the index row must be a Number. ´rows < currentID-maxRows´ will be deleted.\n- `maxStore`maximum number of rows stored by this node for replay if a client connects. If grater than **0** the amount of rows in flow context is limited. for this to work the index row must be a Number. ´rows < currentID-maxStore´ will be deleted.\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\n```json\n{\n \"command\": \"delete\",\n \"object\": \"columnOrder\"\n}\n```\n\n- `deleteTable` tableCache\n- `deleteRow` delete single row. `object` matching index property\n- `ignoreRow` delete single row and put it on an ignore list. `object` matching index property\n- `unIgnoreRow`remove row from the ignore list. `object` matching index property\n- `unIgnoreRows`delte the ignore list. \n- `deleteRowOrder` delete custom row order\n- `deleteColumnOrder` delete custom column order\n This is important if you add or delete columns in the tabulator config otherwise the columns most likely don`t show up\n- `deleteColumnWidth` delete custom column width\n- `columnHide` hide a column. `object` matching column field\n- `columnUnHide` unhide a column. `object` matching column field\n- `columnsUnHide` unhide all hidden columns.\n- `setMaxStore` set maximum amount of rows in cache\n- `setMaxDisplay` set maximum amout of rows in ui-table\n- `getTable` get table data (as displayed) as an array (on 2nd output)\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":"maxRows","type":"num","value":"0","ui":{"icon":"font-awesome/fa-list-ol","type":"input","opts":{"types":["num","bool","env"]}}},{"name":"maxStore","type":"num","value":"0","ui":{"icon":"font-awesome/fa-database","type":"input","opts":{"types":["num","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\",\"storage\":\"file\"},\"tableEdit\":{\"name\":\"tableEdit\",\"storage\":\"file\"}}","ui":{"icon":"font-awesome/fa-database","label":{"en-US":"Context"},"type":"input","opts":{"types":["json","env"]}}}],"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\");\nvar maxRows = env.get(\"maxRows\") || 0;\nvar maxStore = env.get(\"maxStore\") || 0;\n\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\nif (tableConfig.hasOwnProperty(\"maxStore\")) maxStore=tableConfig.maxStore;\nif (tableConfig.hasOwnProperty(\"maxRows\")) maxRows=tableConfig.maxRows;\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// merge edits into a destination object respecting _children\nvar mergeEdits = function(destination) {\n \n var mergeChildEdits = function(children) {\n children.forEach(child => {\n if (child.hasOwnProperty(tableIndex) && tableEdit.hasOwnProperty(child[tableIndex])) {\n// node.warn([\"mergeChild\",child])\n Object.keys(tableEdit[child[tableIndex]]).forEach(edit => {\n if (child.hasOwnProperty(edit)) {\n child[edit]=tableEdit[child[tableIndex]][edit];\n// node.warn([\"mergeChild edit \",edit,child[edit]])\n }\n });\n }\n if (child.hasOwnProperty(\"_children\")) {\n mergeChildEdits(child._children);\n }\n })\n }\n \n\n Object.keys(destination).forEach(row => {\n if (destination[row].hasOwnProperty(tableIndex)) {\n if (tableEdit.hasOwnProperty(row)) {\n Object.keys(tableEdit[row]).forEach(edit => {\n destination[row][edit]=tableEdit[row][edit];\n });\n }\n if (destination[row].hasOwnProperty(\"_children\")) {\n mergeChildEdits(destination[row]._children);\n }\n }\n });\n}\n\n// deep search for a column including nested columns\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\":msg.tabulatorCommand || \"updateOrAddData\",\n \"arguments\": [],\n \"returnPromise\": false\n};\n\n// store data in tableData\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 if (maxRows>0 && Object.keys(tableData).lenght===0) {\n tableConfig.currentFirstRow=msg.topic;\n }\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 if (maxStore>0 && typeof msg.topic === \"number\") { // limit rows in tableData\n let rowKeys = Object.keys(tableData);\n if (rowKeys.length>maxStore) {\n for (let i=0; i<(rowKeys.length-maxStore); i++) {\n delete tableData[rowKeys[i]];\n }\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 // add aditional parameters\n if (msg.hasOwnProperty(\"tabulatorParameter\") && Array.isArray(msg.tabulatorParameter)) {\n for (let arg in msg.tabulatorParameter) msgToTable.payload.arguments.push(arg);\n }\n // delete rows if rows exceed maxRows\n /*\n if (maxRows>0 && tableConfig.hasOwnProperty(\"currentFirstRow\") && typeof tableData[msg.topic][tableIndex]===\"number\") {\n //node.warn([maxRows,tableConfig.hasOwnProperty(\"currentFirstRow\"),typeof tableData[msg.topic][tableIndex],tableConfig.currentFirstRow,tableData[msg.topic][tableIndex]-maxRows])\n if (tableConfig.currentFirstRow<tableData[msg.topic][tableIndex]-maxRows) {\n node.warn([\"maxRowExeeded\",tableConfig.currentFirstRow]);\n node.send([null,{payload:{\"command\":\"deleteRow\",\"arguments\": [tableConfig.currentFirstRow],\"returnPromise\": false}},null]);\n tableConfig.currentFirstRow++;\n }\n }*/\n if (maxRows>0 && typeof tableData[msg.topic][tableIndex]===\"number\" && msg.topic-maxRows>0) {\n node.send([null,{payload:{\"command\":\"deleteRow\",\"arguments\": [msg.topic-maxRows],\"returnPromise\": false}},null]);\n }\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.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\") && msg.payload.command!=='getTable') {\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 'deleteRow':\n case 'deleteDevice':\n var deleteRow = function(id) {\n // check if row is in root\n if (tableData.hasOwnProperty(id)) {\n delete tableData[id]\n return true;\n }\n // check if row is a child\n let deleteChildRow = function(children, id) {\n for(let i = 0; i < children.length; i++){\n if (children[i].hasOwnProperty(tableIndex) && children[i][tableIndex]===id) {\n children.splice(i, 1); \n return true; \n }\n if (children[i].hasOwnProperty(\"_children\")) {\n if (deleteChildRow(children[i]._children,id)) {\n if (children[i]._children.length === 0) {\n delete children[i]._children;\n }\n return true;\n }\n }\n }\n return false;\n };\n \n for (let row in tableData) {\n if (tableData[row].hasOwnProperty(\"_children\")) {\n if (deleteChildRow(tableData[row]._children,id)) return true;\n }\n }\n return false;\n }\n \n if (deleteRow(msg.payload.object)) {\n status.text=msg.payload.object+\" deleted\";\n } else {\n status.fill=\"yellow\";\n status.text=msg.payload.object+\" undefined\";\n }\n break;\n case 'ignoreRow':\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 'unIgnoreRow':\n case 'unIgnoreDevice':\n if (tableConfig.hasOwnProperty('ignoreDevice')) {\n delete tableConfig.ignoreDevice[msg.payload.object];\n }\n break;\n case 'unIgnoreRows':\n case 'unIgnoreDevices':\n delete tableConfig.ignoreDevice;\n break;\n case 'updateData':\n status.text=\"column \"+msg.payload.column+\" updated\";\n delete msg.ui_control;\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 'deleteColumnOrder':\n case 'restoreColumnOrder':\n delete tableConfig.columns;\n break;\n case 'deleteColumnWidth':\n case 'resetColumnWidth':\n delete tableConfig.columnWidths;\n break;\n case 'setResponsiveLayout':\n tableConfig.ResponsiveLayout=!tableConfig.ResponsiveLayout;\n break;\n case 'deleteRowOrder':\n delete tableConfig.rowOrder;\n break;\n case 'setMaxStore':\n tableConfig.maxStore=msg.payload.object;\n maxStore=msg.payload.object;\n break;\n case 'setMaxRows':\n tableConfig.maxRows=msg.payload.object;\n maxRows=msg.payload.object;\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\n let command = msg.payload.command;\n var tableArray;\n if (command===\"getTable\") {\n msg.payload.tableArray=[];\n tableArray=msg.payload.tableArray\n } else {\n msg.payload=[];\n tableArray=msg.payload;\n }\n \n var pushRowData = function(rowData) {\n // ignore rows in ignoreRows array\n if (tableConfig && tableConfig.hasOwnProperty(\"ignoreDevice\") && tableConfig.ignoreDevice[rowData]) {\n // do nothing\n } else {\n // merge edits into table\n if (tableEdit && tableEdit.hasOwnProperty(rowData)) {\n let tableRow = RED.util.cloneMessage(tableData[rowData]);\n Object.keys(tableEdit[rowData]).forEach((field) => {\n tableRow[field]=tableEdit[rowData][field];\n });\n tableArray.push(tableRow);\n } else {\n tableArray.push(tableData[rowData]);\n }\n }\n }\n \n if (tableConfig.hasOwnProperty(\"rowOrder\")) {\n // first check if new rows exits which are not in rowOrder\n Object.keys(tableData).forEach((key) => {\n if (tableConfig.rowOrder.indexOf(tableData[key][tableIndex])<0) {\n tableConfig.rowOrder.push(tableData[key][tableIndex]); // add row to the end of rowOrder\n }\n });\n tableConfig.rowOrder.forEach((value,index) => {\n node.warn([\"pushRowOrder\",value,tableData.hasOwnProperty(value),tableData[value]]);\n if (tableData.hasOwnProperty(value)) { // push rows in rowOrder sequence\n pushRowData(value);\n } else { // delete not existing rows from rowOrder\n tableConfig.rowOrder.splice(index,1)\n }\n });\n } else {\n for (let rowData in tableData) {\n pushRowData(rowData);\n }\n }\n // store the first index if maxRows limits amount of displayed lines\n if (maxRows>0 && tableData) {\n let tableKeys=Object.keys(tableData);\n if (tableKeys.length>0 && typeof tableData[tableKeys[0]][tableIndex] === \"number\") {\n tableConfig.currentFirstRow=tableData[tableKeys[0]][tableIndex];\n }\n }\n \n if (command=='getTable'){\n status.fill=\"blue\";\n status.text+=\" \"+tableArray.length+\" rows emitted\";\n return [{payload:status},null,msg];\n } else {\n status.fill=\"blue\";\n status.text+=\" \"+tableArray.length+\" rows restored\";\n return [{payload:status},msg,[{topic:\"maxRows\",payload:maxRows},{topic:\"maxStore\",payload:maxStore}]];\n }\n} if (msg.hasOwnProperty(\"ui_control\")) {\n // callback from tabulator\n status.fill=\"blue\";\n status.text=\"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 as edited field\n flow.set(\"$parent.\"+tableContext.tableEdit.name,tableEdit,tableContext.tableEdit.storage);\n mergeEdits(tableData);\n flow.set(\"$parent.\"+tableContext.tableData.name,tableData,tableContext.tableData.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},null,msg]); // was 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 rowData in tableConfig.ignoreDevice) {\n if (tableConfig.ignoreDevice.hasOwnProperty(rowData)) {\n msg.ignoredDevices.push({\"text\":rowData,\"icon\":\"fa fa-plug\",\"topic\":\"unIgnoreDevice\",\"payload\":rowData}) \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 case \"rowMoved\":\n if (tableConfig.rowOrder===undefined) tableConfig.rowOrder={};\n tableConfig.rowOrder=msg.ui_control.rowOrder;\n flow.set(\"$parent.\"+tableContext.tableConfig.name,tableConfig,tableContext.tableConfig.storage);\n status.text=\"new row order\";\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} \nif (Array.isArray(msg.payload)) {\n tableData={};\n \n msg.payload.forEach((row) => {\n if (row.hasOwnProperty(tableIndex)) {\n tableData[row[tableIndex]]=row;\n }\n });\n if (msg.keepEdits) {\n mergeEdits(tableData);\n }\n\n \n flow.set(\"$parent.\"+tableContext.tableData.name,tableData,tableContext.tableData.storage);\n if (tableContext.hasOwnProperty(\"tableEdit\") && !msg.keepEdits) {\n tableEdit={};\n flow.set(\"$parent.\"+tableContext.tableEdit.name,tableEdit,tableContext.tableEdit.storage);\n }\n status.fill=\"blue\"\n status.text=\"table replaced \"+msg.payload.length+\" rows\";\n return [{payload:status},msg,null];\n} \n \n// nothing to do bejond this point\nstatus.text+=\" [\"+msg.payload+\"]\";\nreturn [{payload:status},null];\n","outputs":3,"noerr":0,"initialize":"","finalize":"","x":192,"y":85,"wires":[[],[],[]],"icon":"font-awesome/fa-table"},{"id":"b952ac65.8ddfb","type":"subflow:2924702c.b33a7","z":"626acd09.10ba04","name":"","env":[{"name":"tabulator","value":"{\"customHeight\":18,\"tabulator\":{\"index\":\"id\",\"layout\":\"fitColumns\",\"movableColumns\":true,\"groupBy\":\"\",\"dataTree\":true,\"columns\":[{\"title\":\"<i class='fa fa-tag fa-rotate-90'></i>&nbsp;id\",\"field\":\"id\",\"width\":140,\"frozen\":true,\"tooltip\":true,\"headerSort\":false,\"headerTooltip\":\"Element\",\"headerContext\":\"function(e,column){ this.send({ui_control:{callback:'headerContextNoHide'},position:{\\\"x\\\":e.x,\\\"y\\\":e.y},payload:column._column.field}); e.preventDefault(); }\"},{\"title\":\"Device\",\"field\":\"deviceId\",\"headerContext\":\"function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\\\"x\\\":e.x,\\\"y\\\":e.y},payload:column._column.field}); e.preventDefault(); }\"},{\"title\":\"Node\",\"field\":\"nodeId\",\"headerContext\":\"function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\\\"x\\\":e.x,\\\"y\\\":e.y},payload:column._column.field}); e.preventDefault(); }\"},{\"title\":\"Property\",\"field\":\"propertyId\",\"headerContext\":\"function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\\\"x\\\":e.x,\\\"y\\\":e.y},payload:column._column.field}); e.preventDefault(); }\"},{\"title\":\"Program\",\"field\":\"programId\",\"headerContext\":\"function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\\\"x\\\":e.x,\\\"y\\\":e.y},payload:column._column.field}); e.preventDefault(); }\"},{\"title\":\"<i class='fa fa-map-marker'></i>&nbsp;Bezeichung\",\"field\":\"label\",\"width\":100,\"headerTooltip\":\"Alias Name\",\"tooltip\":true,\"headerSort\":true,\"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\":{\"outputFormat\":\"HH:mm\",\"inputFormat\":\"x\",\"invalidPlaceholder\":\"\"},\"title\":\"<i class='fa fa-undo'></i>&nbsp;Start\",\"field\":\"start\",\"topCalc\":\"function(values, data, calcParams){ var secs = 0; var pad = function (num) { return ('0'+num).slice(-2); }; values.forEach(function(value){ secs+=Number(value); }); var minutes = Math.floor(secs / 60); secs = secs%60; var hours = Math.floor(minutes/60); minutes = minutes%60; return pad(hours)+':'+pad(minutes)+':'+pad(secs); }\",\"editor\":\"number\",\"formatter\":\"datetime\",\"width\":40,\"headerSort\":true,\"headerTooltip\":\"Startzeit\",\"headerContext\":\"function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\\\"x\\\":e.x,\\\"y\\\":e.y},payload:column._column.field}); e.preventDefault(); }\"},{\"title\":\"<i class='fa fa-undo'></i>&nbsp;Dauer\",\"field\":\"duration\",\"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); }\",\"editor\":\"number\",\"editorParams\":{\"min\":1,\"max\":60,\"step\":1},\"width\":40,\"headerSort\":false,\"headerTooltip\":\"Übergangszeit in minuten\",\"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\":[\"#061a00\",\"#0d3300\",\"#134d00\",\"#1a6600\",\"#208000\",\"#269900\",\"#2db300\",\"#33cc00\",\"#39e600\",\"#40ff00\"],\"legend\":\"function (value) {return \\\"<span style='color:#FFFFFF;'>\\\"+value+\\\" %</span>\\\";}\",\"legendColor\":\"#FFFFFF\",\"legendAlign\":\"center\"},\"title\":\"<i class='fa fa-wifi'></i>&nbsp;Wert\",\"field\":\"value\",\"formatter\":\"progress\",\"editor\":\"number\",\"editorParams\":{\"min\":0,\"max\":100,\"step\":5},\"width\":70,\"headerSort\":false,\"headerTooltip\":\"Eingestellter Wert\",\"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 = \\\"#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; } }\",\"rowContext\":\"function(e, row){ this.send({ui_control:{callback:'rowContext'},position:{\\\"x\\\":e.x,\\\"y\\\":e.y},payload:row.getData(),\\\"topic\\\":row.getData().id}); e.preventDefault(); }\",\"cellEdited\":\"function(cell){ this.send({ ui_control:(cell.getColumn().getField()===\\\"value\\\") ? {callback:'valueEdited'} : {callback:'cellEdited'}, payload:cell.getValue(), oldValue:cell.getOldValue(), field:cell.getColumn().getField(), id:cell.getRow().getCell('id').getValue() }); }\",\"rowMoved\":\"function(row){ var rowOrder=[]; row._row.parent.rows.forEach((row,index) => { rowOrder.push(row.data.id); }); this.send({ui_control:{\\\"callback\\\":'rowMoved',\\\"rowOrder\\\":rowOrder}}); }\",\"rowTap\":\"function(e, row){this.send({ui_control:{callback:'rowTap'},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.id}); e.preventDefault();}\"}}","type":"json"},{"name":"tableDataProp","value":"data","type":"str"},{"name":"tableIndex","value":"id","type":"str"},{"name":"dashboard","value":"Pflanzen","type":"str"},{"name":"tableContext","value":"{\"tableData\":{\"name\":\"tableData\",\"storage\":\"file\"},\"tableConfig\":{\"name\":\"tableConfig\",\"storage\":\"file\"},\"tableEdit\":{\"name\":\"tableEdit\",\"storage\":\"file\"}}","type":"json"}],"x":527,"y":3417,"wires":[["97542c5e.8a918","52dbcb2f.420644"],["125d0585.ea7aca"]]}]
@EivindTjessem
Copy link

Hi,
I think this seems to be a very good table handler. But I'm struggling to set up and use.

Can you post a couple of examples of use? (code for import)

  • Ex: Creating a simple table, and removing a row in it

It will make it much easier to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment