Hi all, I did a demo for someone recently and thought I'd share for you all to enjoy (or ignore)
I have not made a contrib node out of it, but that would not be too difficult to do.
Here is a gif for your viewing pleasure:
And more importantly, here is the demo flow:
[{"id":"575c755afa443ca9","type":"tab","label":"Dashboard terminal","disabled":false,"info":"","env":[]},{"id":"7bab5965093a262f","type":"group","z":"575c755afa443ca9","name":"terminal","style":{"stroke":"#000000","stroke-opacity":"0.87","label":true,"fill":"#000000","fill-opacity":"0.35"},"nodes":["fe38a45454245194","c2ab95f1fc818b4e","fad233b0126b26de","f0e5b13cdee7a589","645574ddf19bb3a1","8e472dbf0ce76b58"],"x":154,"y":19,"w":922,"h":122},{"id":"fe38a45454245194","type":"link in","z":"575c755afa443ca9","g":"7bab5965093a262f","name":"Log to Dashboard Teminal","links":["105bce33b15891a5","24c545183d5fb2fe","2526f2d1c4691e31","49d1babaecd648b9","7d9775a88fe34a47","c787c8636bc7379c","d23d3596023583b2","645574ddf19bb3a1","594b1355ce316660","110cb7c42ddbbcb7"],"x":290,"y":100,"wires":[["c2ab95f1fc818b4e"]],"l":true},{"id":"c2ab95f1fc818b4e","type":"function","z":"575c755afa443ca9","g":"7bab5965093a262f","name":"log to context.log","func":"const MAX_ROWS = 200\n\nif (msg.topic === \"clear-log\") {\n context.set('log', [])\n msg.topic = ''\n msg.payload = []\n return msg\n}\n\nconst incomingMessage = msg.payload\nlet log = context.get('log') || []\nif (!Array.isArray(log)) {\n log = []\n}\n\nlet logMessages = normaliseLogEntry(msg.payload)\n\nfunction normaliseLogEntry(data) {\n const result = []\n if (data === null) {\n return normaliseLogEntry(\"null\") \n } else if (typeof data === \"undefined\") {\n return normaliseLogEntry(\"undefined\")\n } else if (typeof data === 'number') {\n return normaliseLogEntry(\"\" + data)\n } else if (Array.isArray(data)) {\n const arrStr = [\"[\"]\n for (let index = 0; index < data.length; index++) {\n if (index > 0) { arrStr.push(', ') }\n arrStr.push(normaliseLogEntry(data[index]))\n }\n arrStr.push(\"]\")\n return normaliseLogEntry(arrStr.join(''))\n } else if (typeof data === 'object') {\n result.push(\"> \" + JSON.stringify(data, null, 2))\n } else if (Buffer.isBuffer(data)) {\n result.push(\"> \" + data.toJSON())\n } else if (data && typeof data !== 'string') {\n result.push(\"> \" + data.toString())\n } else {\n if (data.includes('\\n')) {\n const lines = data.split('\\n').map(m => \"> \" + m)\n result.push(...lines)\n } else {\n result.push(\"> \" + data)\n }\n }\n return result\n}\n\nlog.push(...logMessages.flat())\n\nif(log.length >= MAX_ROWS) {\n // remove the oldest messages which are at the beginning of the array\n log = log.slice(-MAX_ROWS)\n}\n\ncontext.set('log', log)\nmsg.payload = log\nreturn msg\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":100,"wires":[["fad233b0126b26de"]]},{"id":"fad233b0126b26de","type":"ui-template","z":"575c755afa443ca9","g":"7bab5965093a262f","group":"284bc3bf9214f38d","name":"","order":0,"width":0,"height":0,"head":"","format":"<template>\n \n <div class=\"terminal\" ref=\"terminal\">\n <div class=\"log-entry\" v-for=\"(log, index) in logData\" :key=\"index\">\n {{ log }}\n </div>\n </div>\n\n \n <v-btn @click=\"clearLog()\">Clear</v-btn>\n</template>\n\n<script>\n export default {\n data() {\n return {\n logData: []\n }\n },\n watch: {\n msg: function () {\n console.log('watch.msg')\n \n if (Array.isArray(this.msg.payload)) {\n const terminal = this.$refs.terminal;\n const graceDistance = 25 //px\n const isAtBottom = terminal.scrollHeight - terminal.scrollTop - terminal.clientHeight <= graceDistance;\n if (isAtBottom) {\n this.$nextTick(() => {\n terminal.scrollTop = terminal.scrollHeight;\n })\n }\n this.logData = this.msg.payload\n }\n }\n },\n methods: {\n clearLog: function () {\n this.send({topic: \"clear-log\"})\n },\n }\n }\n</script>\n<style scoped>\n .terminal {\n background-color: black;\n color: limegreen;\n font-family: 'Courier New', Courier, monospace;\n padding: 20px;\n border-radius: 5px;\n height: 600px;\n overflow-y: auto;\n box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);\n }\n\n .log-entry {\n margin: 0;\n line-height: 1.5;\n }\n</style>","storeOutMessages":true,"passthru":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":760,"y":100,"wires":[["8e472dbf0ce76b58"]]},{"id":"59cfcb76bbc0dc5c","type":"inject","z":"575c755afa443ca9","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Hello world","payloadType":"str","x":240,"y":240,"wires":[["2526f2d1c4691e31"]]},{"id":"08215b5cb4982c90","type":"inject","z":"575c755afa443ca9","name":"long test","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"This is a long looooooooooooooooooong loooooooooooooooooooooooong loooooooooooooooooooooooooooooooong test message","payloadType":"str","x":240,"y":280,"wires":[["2526f2d1c4691e31"]]},{"id":"2526f2d1c4691e31","type":"link out","z":"575c755afa443ca9","name":"link out 3","mode":"link","links":["fe38a45454245194"],"x":385,"y":280,"wires":[]},{"id":"777cab562491a8a4","type":"comment","z":"575c755afa443ca9","name":"simple test messages","info":"","x":260,"y":200,"wires":[]},{"id":"f0e5b13cdee7a589","type":"inject","z":"575c755afa443ca9","g":"7bab5965093a262f","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"clear-log","x":340,"y":60,"wires":[["c2ab95f1fc818b4e"]]},{"id":"645574ddf19bb3a1","type":"link out","z":"575c755afa443ca9","g":"7bab5965093a262f","name":"link out 10","mode":"link","links":["fe38a45454245194"],"x":1035,"y":100,"wires":[]},{"id":"8e472dbf0ce76b58","type":"switch","z":"575c755afa443ca9","g":"7bab5965093a262f","name":"clear-log?","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"clear-log","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":920,"y":100,"wires":[["645574ddf19bb3a1"]]},{"id":"e6c68773c06acbfb","type":"ui-form","z":"575c755afa443ca9","name":"","group":"192c891a3de910f5","label":"Task Delete","order":0,"width":0,"height":0,"options":[{"label":"Task Name","key":"task","type":"text","required":true,"rows":null},{"label":"Reason","key":"reason","type":"multiline","required":false,"rows":3}],"formValue":{"task":"","reason":""},"payload":"","submit":"Delete","cancel":"","resetOnSubmit":true,"topic":"task-control","topicType":"str","splitLayout":"","className":"","x":770,"y":240,"wires":[["6139cd66dd07d730","90ee03051095e7f3"]]},{"id":"f9fe03e7b03a0695","type":"comment","z":"575c755afa443ca9","name":"Demo form","info":"","x":760,"y":200,"wires":[]},{"id":"594b1355ce316660","type":"link out","z":"575c755afa443ca9","name":"link out 11","mode":"link","links":["fe38a45454245194"],"x":1065,"y":240,"wires":[]},{"id":"6139cd66dd07d730","type":"debug","z":"575c755afa443ca9","name":"debug 14","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":940,"y":280,"wires":[]},{"id":"90ee03051095e7f3","type":"function","z":"575c755afa443ca9","name":"delete (sim)","func":"const sleep = ms => new Promise(r => setTimeout(r, ms));\nconst sendPayload = str => node.send({payload: str});\n\n// send initial message\nsendPayload(`🗑️ Deleting task: '${msg.payload.task}', reason: ${msg.payload.reason}`)\n\n// pretend to do work\nawait sleep('350')\nsendPayload('⚙️ Processing your request. Please wait...')\nawait sleep('1200')\nsendPayload('✅ Huray, Your task was completed successful. Please remember words are useful for saying things on a screen or on paper. In this instance, I am simply using them to generate a long response - for demonstration purposes! 😉')\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":950,"y":240,"wires":[["594b1355ce316660"]]},{"id":"781a18c082aab908","type":"inject","z":"575c755afa443ca9","name":"empty line","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":240,"y":320,"wires":[["2526f2d1c4691e31"]]},{"id":"d084e92102e0c7f8","type":"inject","z":"575c755afa443ca9","name":"object test","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"info\":\"testing object payload with a data array\",\"data\":[1,2,\"three\",4,5]}","payloadType":"json","x":520,"y":240,"wires":[["110cb7c42ddbbcb7"]]},{"id":"3eb46205eaac1e46","type":"inject","z":"575c755afa443ca9","name":"buffer test","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[65,32,115,116,114,105,110,103,32,111,102,32,99,104,97,114,115,32,115,101,110,100,32,97,115,32,97,32,98,117,102,102,101,114]","payloadType":"bin","x":520,"y":280,"wires":[["110cb7c42ddbbcb7"]]},{"id":"29550367b518fc51","type":"comment","z":"575c755afa443ca9","name":"complex test messages","info":"","x":540,"y":200,"wires":[]},{"id":"110cb7c42ddbbcb7","type":"link out","z":"575c755afa443ca9","name":"link out 12","mode":"link","links":["fe38a45454245194"],"x":645,"y":260,"wires":[]},{"id":"d91b1897d6eecd52","type":"inject","z":"575c755afa443ca9","name":"multi line","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"\"\\nGood morning\\n\\nand in case I don't see ya...\\ngood afternoon, good evening and good night!\"","payloadType":"jsonata","x":520,"y":320,"wires":[["110cb7c42ddbbcb7"]]},{"id":"284bc3bf9214f38d","type":"ui-group","name":"Terminal","page":"11a250b56df8dffa","width":"9","height":"1","order":-1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"192c891a3de910f5","type":"ui-group","name":"Tasks","page":"11a250b56df8dffa","width":"3","height":"1","order":-1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"11a250b56df8dffa","type":"ui-page","name":"Page 1","ui":"ac5e535515ebb9c6","path":"/page1","icon":"home","layout":"grid","theme":"f36403a3012e6880","order":1,"className":"","visible":"true","disabled":"false"},{"id":"ac5e535515ebb9c6","type":"ui-base","name":"My Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"navigationStyle":"default","titleBarStyle":"default"},{"id":"f36403a3012e6880","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094CE","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]