I mentioned this in passing in another thread but thought I'd share the progress so far in case anyone is interested.
This came out of a recent discussion about MQTT v5 and it made me realise that it looks as though the author of the excellent MQTT Explorer has lost interest in it. Which is a shame and it means that is seems unlikely that it will get v5 support.
Also, it seemed like an interesting opportunity to push the boundaries of what we can do with MQTT and Node-RED along with creating an exercise for uibuilder and an opportunity to learn some more Svelte. Phew!
So there is a looong way to go to make this something worthy of MQTT Explorer. However, as a proof of concept, it is already working in a terribly basic and ugly fashion.
Please note that I'm using uibuilder vNext, I can't say whether it will work with uibuilder v4.
Firstly let me share the Svelte source files and main.js. These all go in the src folder for your uibuilder node instance. I will post the instructions for using Svelte in the next post (they will be included in the Tech Docs for uibuilder v5).
Example output
src/main.js
/** Main entry point. App is the starting point for our Svelte app */
import App from './App.svelte';
// @ts-ignore
const app = new App({
// Where will we attach our app?
target: document.body,
// What data properties do we want to pass?
// Props can also be defined in the .svelte file
props: {
nrMsg: ''
}
});
export default app;
src/App.svelte
<script>
import { onMount } from 'svelte'
import MqttView from './MqttView.svelte'
export let uibsend
export let nrMsg
let tree2 = {}
let showTree2
let newMsg
/** Simple HTML JSON formatter
* @param {json} json The JSON or JS Object to highlight
*/
function syntaxHighlight(json) {
json = JSON.stringify(json, undefined, 4)
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
var cls = 'number'
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key'
} else {
cls = 'string'
}
} else if (/true|false/.test(match)) {
cls = 'boolean'
} else if (/null/.test(match)) {
cls = 'null'
}
return '<span class="' + cls + '">' + match + '</span>'
})
return json
} // --- End of syntaxHighlight --- //
onMount(() => {
uibuilder.start()
uibsend = uibuilder.eventSend
uibuilder.onChange('msg', function(msg){
nrMsg = syntaxHighlight(msg)
newMsg = msg
})
}) // --- End of onMount --- //
</script>
<main>
<MqttView newMsg={newMsg} />
</main>
<hr>
<pre id="msg" class="syntax-highlight">{@html nrMsg}</pre>
<style>
pre {
margin: auto;
width: fit-content;
max-width: 720px;
overflow-x: auto;
max-height: 30em;
overflow-y: auto;
}
</style>
src/MqttView.svelte
<script>
/** Draw a single level of an MQTT hierarchy of messages & recurse to the next level if needed
* Calls itself recursively to walk down each element of the MQTT topic (separated by /).
*
*/
// import { slide } from 'svelte/transition'
//#region --- Props ---
// Take update msgs from Node-RED - used for the top level request
export let newMsg = null
// Alternatively take the tree - used for recursive requests
export let tree = {}
//#endregion --- Props ---
// Holder for an HTML formatted view of the tree
//let showTree
// Holds the expanded state for each entry
const _expansionState = { /* treeNodeId: expanded <boolean> */ }
const toggleExpansion = (item) => {
_expansionState[item] = _expansionState[item] ? !_expansionState[item] : true
}
/** Simple HTML JSON formatter
* @param {json} json The JSON or JS Object to highlight
*/
function syntaxHighlight(json) {
json = JSON.stringify(json, undefined, 4)
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
var cls = 'number'
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key'
} else {
cls = 'string'
}
} else if (/true|false/.test(match)) {
cls = 'boolean'
} else if (/null/.test(match)) {
cls = 'null'
}
return '<span class="' + cls + '">' + match + '</span>'
})
return json
} // --- End of syntaxHighlight --- //
/** Update the tree from an incoming MQTT msg via Node-RED
* @param {object} msg A node-red msg obj containing an MQTT message
*/
function updTree(msg) {
const splitTopic = msg.topic.split('/')
// Walk down the tree of topics
let lvl = tree
for (let i = 0; i < splitTopic.length; i++) {
let el = splitTopic[i]
if ( ! lvl[el] ) lvl[el] = {}
//else lvl[el].newVal = msg
lvl = lvl[el]
}
// if ( splitTopic[0] === 'DEV' ) console.log(msg)
// At the final root of the topic tree, add the data
// try {
// lvl.payload = JSON.parse(msg.payload)
// } catch (e) {
// lvl.payload = msg.payload
// }
lvl.newVal = {
payload: msg.payload,
qos: msg.qos,
retain: msg.retain,
contentType: msg.contentType,
correlationData: msg.correlationData,
messageExpiryInterval: msg.messageExpiryInterval,
responseTopic: msg.responseTopic,
userProperties: msg.userProperties,
topic: msg.topic,
}
//console.log(tree)
// NB: updating lvl, updates the tree var
}
//console.log('>> tree >>', tree.length, tree)
let entries = []
$: {
try {
// console.log('>> newMsg >>', newMsg)
if ( newMsg !== null ) {
updTree(newMsg)
} //else console.log(tree)
// console.log('>> tree >>', tree.length, tree)
entries = Object.keys(tree)
delete entries.newVal
//console.log('>> entries >>', entries.length, entries)
} catch (e) {
console.log(`[MqttView] Error. ${e.message}`)
}
// try {
// showTree = syntaxHighlight(tree)
// } catch (e) {
// console.error(`[MqttView] Error. ${e.message}`)
// }
/////// tree[item] ? tree[item].payload : '--'
}
//$: arrowDown = expanded
</script>
<ul><!-- transition:slide -->
{#each entries as item}
{#if item !== 'newVal'}
<li>
{#if tree[item] !== undefined && tree[item] !== null && tree[item].constructor.name === 'Object' }
<b on:click={toggleExpansion(item)}>
<span class="arrow {_expansionState[item]?'arrowDown':''}">▶</span>
{item}
</b>:
{#if _expansionState[item]}
<svelte:self tree={tree[item]} />
{#if tree[item].newVal }
<table style="border:1px solid silver;margin-left:1rem;font-size: 60%;">
{#each Object.entries(tree[item].newVal) as [title, value]}
{#if value !== undefined }
<tr>
<th class="leaf">{title}</th>
<th class="value">{ value }</th>
</tr>
{/if}
{/each}
</table>
{/if}
{/if}
{:else}
<span class="leaf">{item}</span>: <span class="value">{ tree[item] }</span>
{/if}
</li>
{/if}
{/each}
</ul>
<!-- {#if newMsg !== null }
<pre id="tree" class="syntax-highlight">{@html showTree}</pre>
{/if} -->
<style>
ul {
margin: 0;
list-style: none;
padding-left: 1.2rem;
user-select: none;
}
.no-arrow { padding-left: 1.0rem; }
.arrow {
cursor: pointer;
display: inline-block;
/* transition: transform 200ms; */
}
.arrowDown { transform: rotate(90deg); }
pre {
width: 98%;
max-width: 98%;
overflow-x: auto;
overflow-y: auto;
max-height: 10em;
}
</style>