Proof of concept for a replacement for MQTT Explorer

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
		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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
		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':''}">&#x25b6</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>
4 Likes

And now the build config and template

package.json

{
  "name": "svelte-app",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w",
    "start": "sirv public --no-clear"
  },
  "devDependencies": {
    "@rollup/plugin-commonjs": "^17.0.0",
    "@rollup/plugin-node-resolve": "^11.0.0",
    "rollup": "^2.3.4",
    "rollup-plugin-css-only": "^3.1.0",
    "rollup-plugin-livereload": "^2.0.0",
    "rollup-plugin-svelte": "^7.0.0",
    "rollup-plugin-terser": "^7.0.0",
    "svelte": "^3.0.0"
  },
  "dependencies": {
    "sirv-cli": "^2.0.0"
  }
}

rollup.config.js

import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import css from 'rollup-plugin-css-only';

// Added for uibuilder - the build output folder
const uibDist = 'dist'

const production = !process.env.ROLLUP_WATCH;

function serve() {
	let server;

	function toExit() {
		if (server) server.kill(0);
	}

	return {
		writeBundle() {
			if (server) return;
			server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
				stdio: ['ignore', 'inherit', 'inherit'],
				shell: true
			});

			process.on('SIGTERM', toExit);
			process.on('exit', toExit);
		}
	};
}

export default {
	input: 'src/main.js',
	output: {
		sourcemap: true,
		format: 'iife',
		name: 'app',
		file: `${uibDist}/build/bundle.js`
	},
	plugins: [
		svelte({
			compilerOptions: {
				// enable run-time checks when not in production
				dev: !production
			}
		}),
		// we'll extract any component CSS out into
		// a separate file - better for performance
		css({ output: 'bundle.css' }),

		// If you have external dependencies installed from
		// npm, you'll most likely need these plugins. In
		// some cases you'll need additional configuration -
		// consult the documentation for details:
		// https://github.com/rollup/plugins/tree/master/packages/commonjs
		resolve({
			browser: true,
			dedupe: ['svelte']
		}),
		commonjs(),

		// In dev mode, call `npm run start` once
		// the bundle has been generated
		!production && serve(),

		// Watch the `public` directory and refresh the
		// browser on changes when not in production
		!production && livereload(uibDist),

		// If we're building for production (npm run build
		// instead of npm run dev), minify
		production && terser()
	],
	watch: {
		clearScreen: false
	}
};

dist/index.html

<!DOCTYPE html><html lang="en"><head>

	<meta charset='utf-8'>
	<meta name='viewport' content='width=device-width,initial-scale=1'>

	<title>MQTT Explorer</title>

	<link rel='icon' type='image/png' href='./favicon.png'>
	<link rel='stylesheet' href='./global.css'>
	<link rel='stylesheet' href='./build/bundle.css'>

	<script defer src="../uibuilder/vendor/socket.io/socket.io.js"></script>
    <script defer src="./uibuilderfe.min.js"></script>
	<script defer src='./build/bundle.js'></script>

</head><body>
</body></html>

dist/global.css

/* Import global uibuilder styles (optional) */
@import url("./uib-styles.css");

Now an example flow.

Note that the MQTT configuration node used is set to v5 so you need a v5 compatible broker if you want to see everything. You can easily change back to v3 though. Mosquitto v1.6.4 or above should support v5.

[{"id":"4393d057899d5287","type":"tab","label":"MQTT Viewer","disabled":false,"info":"","env":[]},{"id":"9f1e743afa926865","type":"uibuilder","z":"4393d057899d5287","name":"","topic":"","url":"mqtt-explorer","fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"templateFolder":"blank","extTemplate":"","showfolder":false,"useSecurity":false,"allowUnauth":false,"allowAuthAnon":false,"sessionLength":432000,"tokenAutoExtend":false,"reload":false,"sourceFolder":"dist","deployedVersion":"5.0.0-dev.2","x":780,"y":200,"wires":[["7d8dc679995a88bd"],["affb2d9a74d04af4"]]},{"id":"c97df9205436c418","type":"mqtt in","z":"4393d057899d5287","name":"","topic":"#","qos":"2","datatype":"auto","broker":"1fe2972256cb8a87","nl":false,"rap":true,"rh":0,"inputs":0,"x":190,"y":180,"wires":[["ae90065eb89d6ad5"]]},{"id":"ae90065eb89d6ad5","type":"switch","z":"4393d057899d5287","name":"","property":"topic","propertyType":"msg","rules":[{"t":"cont","v":"nrlog","vt":"str"},{"t":"cont","v":"telegraf","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":3,"x":330,"y":180,"wires":[[],[],["046efcf288d44964"]]},{"id":"a8081162a034775c","type":"mqtt in","z":"4393d057899d5287","name":"","topic":"$SYS/#","qos":"2","datatype":"auto","broker":"1fe2972256cb8a87","nl":false,"rap":true,"rh":0,"inputs":0,"x":190,"y":240,"wires":[[]]},{"id":"7d8dc679995a88bd","type":"debug","z":"4393d057899d5287","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":950,"y":180,"wires":[]},{"id":"affb2d9a74d04af4","type":"debug","z":"4393d057899d5287","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":950,"y":220,"wires":[]},{"id":"046efcf288d44964","type":"delay","z":"4393d057899d5287","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"10000","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":570,"y":200,"wires":[["9f1e743afa926865"]]},{"id":"9e5b589642d865dc","type":"mqtt out","z":"4393d057899d5287","name":"","topic":"DEV/m5test","qos":"","retain":"","respTopic":"DEV/m5test/response","contentType":"text/plain","userProps":"{\"user\":\"props\"}","correl":"Correlation_Data","expiry":"500","broker":"1fe2972256cb8a87","x":650,"y":400,"wires":[]},{"id":"fafd6b4f93e8f2ed","type":"inject","z":"4393d057899d5287","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":480,"y":400,"wires":[["9e5b589642d865dc"]]},{"id":"6f4498690164cb8e","type":"mqtt in","z":"4393d057899d5287","name":"","topic":"DEV/m5test","qos":"2","datatype":"auto","broker":"1fe2972256cb8a87","nl":false,"rap":true,"rh":0,"inputs":0,"x":490,"y":480,"wires":[["b526b2468f4af224"]]},{"id":"b526b2468f4af224","type":"debug","z":"4393d057899d5287","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":630,"y":480,"wires":[]},{"id":"328ce5c5619a387a","type":"debug","z":"4393d057899d5287","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":550,"y":120,"wires":[]},{"id":"1fe2972256cb8a87","type":"mqtt-broker","name":"mqtt5-testing","broker":"home.knightnet.co.uk","port":"1883","clientid":"desktop-nr-mqtt5-testing","autoConnect":true,"usetls":false,"protocolVersion":"5","keepalive":"60","cleansession":true,"birthTopic":"DEV/mqtt5-testing","birthQos":"0","birthRetain":"false","birthPayload":"Online","birthMsg":{},"closeTopic":"DEV/mqtt5-testing","closeQos":"0","closeRetain":"false","closePayload":"Offline","closeMsg":{},"willTopic":"DEV/mqtt5-testing","willQos":"0","willRetain":"false","willPayload":"Broken","willMsg":{},"sessionExpiry":""}]

Svelte/ubuilder config instructions


  1. Create a new uibuilder instance and change the url to svelte (or whatever you want). Click on Deploy.

  2. Re-open the node's configuration panel and change the "Serve" setting from src to dist and re-deploy.

  3. Open a command line to the instance root folder, e.g. ~/.node-red/uibuilder/svelte

  4. Install the default svelte template with the command npx degit sveltejs/template . --force.

    WARNING: This will overwrite any existing package.json and README.md files in the instance root folder. So rename those first if you want to retain them. The src folder is also updated with App.svelte and main.js files. A scripts folder and .gitignore, rollup.config.js files are also added.

  5. Rename the public folder to dist.

  6. Make some very minor changes to the rollup config: Just change the output file destination from public/build/bundle.js to dist/build/bundle.js and the line !production && livereload('public') to !production && livereload('dist').

  7. Change the dist/index.html - noting the leading . or .. added to the various resources.

    Note that the html file is simply a template. All of the content is dynamically created.

    <!DOCTYPE html><html lang="en"><head>
        <meta charset='utf-8'>
        <meta name='viewport' content='width=device-width,initial-scale=1'>
    
        <title>Svelte+uibuilder app</title>
    
        <link rel='icon' type='image/png' href='./favicon.png'>
    
        <link rel='stylesheet' href='./global.css'>
        <link rel='stylesheet' href='./build/bundle.css'>
    
        <script defer src="../uibuilder/vendor/socket.io/socket.io.js"></script>
        <script defer src="./uibuilderfe.min.js"></script>
        <script defer src='./build/bundle.js'></script>
    </head><body>
    </body></html>
    

    You may also want to add @import url("./uib-styles.css"); if you want to pick up the default uibuilder styles.

    And in src/App.svelte:

    <script>
        import { onMount } from 'svelte'
    
        export let uibsend
        export let nrMsg = ''
        export let myGreeting = 'Hello there!'
    
        /** 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
            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){
                console.info('msg received from Node-RED server:', msg)
                nrMsg = msg.payload
            })
    
        }) // --- End of onMount --- //
    
    </script>
    
    <main>
        <h1>Svelte + uibuilder</h1>
    
        <p>{myGreeting}</p>
    
        <button on:click={uibsend} data-greeting="{myGreeting}" data-something="this is something">
            Click Me
        </button>
    </main>
    <pre id="msg" class="syntax-highlight">{@html nrMsg}</pre>
    
    <style>
        main {
            text-align: center;
            padding: 1em;
            max-width: 240px;
            margin: 0 auto;
        }
    
        main > h1 {
            /* Assumes you've loaded ./uib-styles.css */
            color: rgb(var(--uib-color-primary));
            text-transform: uppercase;
            font-size: 4em;
            font-weight: 100;
        }
    
        @media (min-width: 640px) {
            main {
                max-width: none;
            }
        }
    
        pre {
            margin: auto;
            max-width:fit-content;
        }
    </style>
    
  8. From the instance root folder run the command npm install && npm run dev

    Note that the dev process creates its own web server but you should ignore that. Just leave it running if you want to have your web page auto-reload when you make changes to the files in src.

  9. Now load the uibuilder page with http://127.0.0.1:1880/svelte/ (or wherever yours ends up)

Marvel at the amazing dynamic, data-driven web app you just made!


OK, so not the most amazing thing. But lets note a couple of important points.

  • Make a change to the text in the App.svelte page and save it - notice anything on your web page?

    Yup, it changed without you having to reload it! Just like Svelte's own dev server :grin:

  • Attach a debug node to the output of your uibuilder node. Make sure it is set to show the whole msg object. Now click on the button on your page. Notice that you get a message just by clicking the button, no code required (other than the HTML for the button itself).

    That uses the new eventSend function in uibuilder v3.2

    Note how the data-xxxx attributes are sent back to node-red in the payload, one of which is dynamic thanks to Svelte. Also note that the msg.uibDomEvent.sourceId in node-red contains the text of the button. Try adding an id attribute to the button to see what difference it makes.

  • Send a msg to the uibuilder node and note how the payload appears on the page

    5 lines of code in total to do that :grin:

19 lines of actual code for a simple, data-driven web page. Not too bad I think.


When I get a chance, I will create a GitHub repository with a uibuilder-specific Svelte template to get rid of some of the above steps.

I will also be adding some features to uibuilder in a future release that will make installing your own (or anyone elses) templates to a uibuilder instance. Also, there will be an install and a build button. So that most of the above steps will be reduced to a couple of clicks. These changes will help anyone who needs a build step for their web app, not just for Svelte users.

1 Like

Slowly but surely improving:

Read-only at the moment of course. Next I need to improve the right-hand side but you can already see that I've got access to the MQTT v5 properties.

10 Likes

This is really cool, @TotallyInformation. I especially like the colors.

I understand you've got a lot of work to do so forgive me if I'm jumping the gun in asking will this be a standalone app like MQTT Explorer at some point?

Love the effort, and MQTT explorer has been a fantastic troubleshooting tool, I believe you were the one from where I'd learned of it, but what does v5 bring to the party over v3.1? Seems to me to be "enterprise" features that seem to kill the basic simple beauty of MQTT.

I've considered it. Maybe at some point, it wouldn't probably take too much work. However, for the time-being it will probably take me a while to get beyond something simple. Node-RED makes the basics for something like this really easy. But it still takes a lot of work to get all the parts working.

Well, that's been covered in a few other places and there is loads online. The previous thread that started this covered some of this explained a number of possible advantages.

1 Like

I think here is a good overview

2 Likes

Thanks. The dark background actually comes from the uibuilder vNext default CSS which automatically adapts to light/dark depending on your browser settings :grinning:

1 Like

@TotallyInformation Your ability to repeatedly jump in the deep end of non-trivial new tech and then turning the knowledge into something non-trivial and meaningful in the process is just mind boggling.

On paper I should be an experienced frontend oriented developer but just taking a quick look on Svelte's documentation makes my brain melt in despair. Just trying to grasp modern day CSS feels so overwhelming I'm actively avoiding it and instead focusing on code reviews and "the frontend code that's between frontend and the backend".

You're a living proof impostor syndrome is not an illusion like some try to convince. :face_with_spiral_eyes:

Haha, don't worry, I've lived with that for most of my working life :wink:

I spent many years as a contractor and consultant and could never believe that people paid me good money to do things that they really could have done themselves. And I've always been really open that I'm only ever 1 page ahead on Google :grinning:

I think I'm probably too dumb to realise that I can't do something so I keep stumbling around until things click together. If I can't understand something, either I find a different explanation that is a bit more understandable or I let things whir away in the back of my head for a bit to see what comes back out.

I'm certainly no expert and I'll probably never be given that I have to keep my brain so full of things for my day-job. But a lot of things in CSS are simpler than people make out. Take dark-mode. I've seen a load of really complex solutions which just seemed crazy so I've not bothered. But then I discovered that the browser actually has a hint as to what you prefer - simple. Then I realised I wanted to merge some variables - most of the explanations require tons of manual entries which again seem crazy - then I discovered that CSS already has the answer and combining info from a few articles resulted in the default CSS. Similarly, most CSS resets are banana's containing far too many resets that will never have an effect. So I just start with something pretty simple from putting together a couple of different articles that do most of what I think might be needed regularly - the rest can wait, why over-complicate things?

Bingo - not perfect but gets the basic job done. Doubtless will grow and develop but looks OK with minimal work:

/** Default Styles for uibuilder
 * see https://css-tricks.com/a-complete-guide-to-custom-properties/ for details on using custom css properties
 * @author: Julian Knight (TotallyInformation)
 * @license: Apache 2.0
 * @created: 2022-01-30
 *
 * NOTES:
 * - Watch out for the order of loading of style sheets!
 * - uibuilder attempts to auto-load this sheet BUT you may need to do it manually to get the right order.
 * - Include in your index.css file as `@import url("./uib-styles.css");`
 */

 /* :root applies to everything */
:root {
    --uib-color-light: 201, 209, 217;
    --uib-color-light-30: 201, 209, 217, 0.3;
    --uib-color-dark:  36, 36, 36;
    --uib-color-medium-dark:  56, 56, 56;
    --uib-color-info:  23, 162, 184; /* Cyan  */
    --uib-color-warn:  255, 193, 7;  /* Amber */
    --uib-color-error: 220, 53, 69;  /* Red   */
    --uib-color-primary:   0, 123, 255;   /* Blue  */
    --uib-color-secondary: 108, 117, 125; /* Grey  */
    --uib-color-success:   40, 167, 69;   /* Green */
    /* Use these for lightening backgrounds using background-blend-mode: lighten; */
    --uib-lum-22: hsl(0, 0%, 22%);
    --uib-lum-28: hsl(0, 0%, 28%);
    --uib-lum-78: hsl(0, 0%, 78%);
    --uib-lum-72: hsl(0, 0%, 72%);

    --uib-color-fg: var(--uib-color-dark);
    --uib-color-bg: var(--uib-color-light-30);
}

/* If the browser reports it, we can adjust for light/dark theme preferences */
@media (prefers-color-scheme: light) {
    :root {
        --uib-color-fg: var(--uib-color-dark);
        --uib-color-fg-lighter: var(--uib-color-medium-dark);
        --uib-color-bg: var(--uib-color-light-30);
        --uib-color-bg-lighter: var(--uib-color-light);
        --uib-lum-lighten: linear-gradient(var(--uib-lum-72),var(--uib-lum-78),var(--uib-lum-72));
        --uib-border: rgb(var(--uib-color-dark));
        --uib-link: blue; /*-webkit-link;*/
        --uib-link-visited: purple;
    }
}
@media (prefers-color-scheme: dark) {
    :root {
        --uib-color-fg: var(--uib-color-light);
        --uib-color-fg-lighter: var(--uib-color-light);
        --uib-color-bg: var(--uib-color-dark);
        --uib-color-bg-lighter: var(--uib-color-medium-dark);
        --uib-lum-lighten: linear-gradient(var(--uib-lum-22),var(--uib-lum-28),var(--uib-lum-22));
        --uib-border: rgb(var(--uib-color-light));
        --uib-link: rgb(135, 206, 250);
        --uib-link-visited: rgb(109, 75, 141);
    }
}

/*#region --- Basic reset --- */

/* * {
    margin: 0;
    padding: 0;
} */
html {
    line-height: 1.5;
    font-size: 100%;
}
body {
    margin-left: 1rem;
    margin-right: 1rem;
    /* padding: 0.3rem; */
    font-family: sans-serif;
    background-color: rgb(var(--uib-color-bg));
    color: rgb(var(--uib-color-fg));
}
h1, h2, h3, h4, h5, h6, heading {
    line-height: 1.3;
}
img, picture, video, canvas, svg {
    /* display: block; */
    object-fit: cover;
    vertical-align: bottom;
    max-width: 100%;
    background-color: grey;
}
p, h1, h2, h3, h4, h5, h6, heading, li, dl, dt, blockquote {
    overflow-wrap: break-word;
    hyphens: auto;
    word-break: break-word;
}
div > p {
    margin-left: 1rem;
    margin-right: 1rem;
}
button, input[type="button" i] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: none;
    padding: .5rem 1rem;
    text-decoration: none;
    background-color: rgb(var(--uib-color-info));
    color: rgb(var(--uib-color-dark));
    font-family: inherit;
    font-size: 1rem;
    line-height: 1.1;
    cursor: pointer;
    text-align: center;
    transition: background 250ms ease-in-out, transform 150ms ease;
    -webkit-appearance: none;
    -moz-appearance: none;
    border-radius: 8px;
    /* box-shadow: 0 3px 5px rgb(var(--uib-color-fg), 0.5); */
    box-shadow: inset 2px 2px 3px rgba(255,255,255, .3),
                inset -2px -2px 3px rgba(0,0,0, .3);
}
button:hover, input[type="button" i]:hover {
    background-color: rgb(var(--uib-color-info), .5);
}

button:focus, input[type="button" i]:focus {
    outline: 1px solid rgb(var(--uib-color-fg));
    outline-offset: -4px;
}

button:active, input[type="button" i]:active {
    transform: scale(0.97);
}
table {
    border-collapse: collapse;
    border: 1px solid var(--uib-border);
    margin-top: 1rem;
    margin-bottom: 1rem;
}
thead th {
    color: rgb(var(--uib-color-bg-lighter));
    background: rgb(var(--uib-color-fg-lighter));
}
th, td {
    padding: .5rem;
}
input, button, textarea, select {
    font: inherit;
}
a:link {
    color: var(--uib-link);
}
a:visited {
    color: var(--uib-link-visited);
}

/*#region --- Basic reset --- */
1 Like

There's brief periods of time when it fades away but then either a) a fresh 10x developer/consultant joins the team, produces cleaner code five times as fast, b) a new project starts and after the first month you realize what actually needs to be done or c) you read a blog post or some community forum on your leasure time that makes you realize you don't know shit. :joy:

Both of us know this is just a polite way of saying you have the right mindset and capabiltiy of not to get intimidated, then just make shit happen. :wink:

This is though only if the OS can give a hint to the browser, which might not be the case on Linux (I'd be pleased to find out that is actually works). :slight_smile:

For the record, I've never used a CSS reset. There's always been some CSS guru that's already done the dirty work. But I agree that CSS variables are awesome!

Making things simple requires more skills than anything. Another proof you're no impostor. No way out of this and I'm pertty sure anyone on the forum can agree on this. :eyes:

Thanks for sharing your thought process! It's fascinating (but also a bit depressing :joy:) to see what you come up next. The same applies to other fearless experimenters on this forum, e.g. @Steve-Mcl and @BartButenaers just to name some.

Aside: Lately I haven't been very active on this forum as I've fallen to the dark side and now focusing on learning my way there. I'm still lurking in the background though and definitely still using Node-RED!

4 Likes

Many thanks for your (I think!) kind words :beers:

Only one way to find out of course (well, we already know that's not true but stick with me :grinning: ).

Does the browser you are using have a light/dark mode setting? I think all Chromium-based ones do. The prefers-color-scheme media query is a CSS3 standard so I think most if not all modern browsers will support it.

The hard way to test it is to install uibuilder vNext into Node-RED, add a uibuilder node, set the url, deploy and open the "blank" page. Then change the setting in your browser to light or dark. I keep Firefox set to light and Edge (my daily-driver) set to dark.

I did have another go with HA back in January. I thought it might help me with some other things. But it didn't, I found it too complex to set up, nowhere near as easy as I expected. As I already have most things that I want set up in Node-RED and my home automation doesn't really change that much, there was nothing for me other than a whole new set of troubles and learning and configuration.

1 Like

Almost tricked me! It appears my gut feeling was right:

I've got a dark GTK theme but there's no OS level dark mode toggle as such (at least not on this distro). I didn't find such setting in Chrome either, not even from the flags section unless it's name does not contain "dark". Not that it's a huge issue as I'm running Dark Reader night and day :sunglasses:. Sadly it's a terrible resource hog on some sites.

Click to read off topic HA stuff

Agree 100%. At first it seemed easy but it definitely is not, if you want "everything" to work. A lot of it's core features seem to be still evolving so some things are easy, some not so much. Personally I was hoping it to fill all of these 1) responsive and good looking dashboard, 2) be relatively easily configurable on tablet, 3) have an Android and iOS app, 4) have admin/non-admin roles, 5) allow setting up basic automations and custom switch to light mappings on mobile/tablet.

With tens of hours of tweaking, it now clears requirements 1-4 quite nicely, so I'm happy enough. Requirement #5 it has failed miserably. My Node-RED based, hand made switch->light mappings respond instantly and work every time but with HA... After wasting three consecutive Sundays, I gave up. It seems like the recommended way to do this works by polling values on fields that are updated via MQTT. It's completely backwards! :smiley:

1 Like

Hi,
Just joined and I would like to thank everyone for their tremendous work and advice that has certainly helped me sort out many issues whilst using the dashboard.

I’m now trying to migrate some of my dashboard layouts using the Svelte/ubuilder config instructions (post 3 above). This is now working on a raspberry pi 400 with 64 bit Raspberry Pi OS, Node-Red v 2.2.2, vivaldi browser and uibuilder vNext

I currently have one issue, after a reboot the browser console log reports :-

:35729/livereload.js?snipver=1:1 Failed to load resource: net:: Err_Connection_Refused

As I understand it, the rollup package uses ‘live re-load ‘on port 35729 as default. My work-around is to add a random number port to the export/default/watch section of the roll-up config so that running npm run dev gets everything working until the next reboot.

My concern is that as I invest more time and effort into refining this set-up this underlying problem may become a real headache, I just don’t know. Any advice on how to permanently resolve this would be much appreciated.
Thanks again !
Andrew

Minor feature request - was trying to view a binary payload but MQTT Explorer doesn't seem to let us do that so can I ask that this one has that facility please

That is a Svelte issue and not terribly surprising since you rebooted the device that was providing the Svelte dev server :wink:

Normally of course, you wouldn't be running the dev server except during development.

I am slowly working to a point where you wouldn't necessarily need a dev server by adding a watch capability into uibuilder. There is already a special message that you can send to uibuilder that will tell the browser to reload the page. You can use that manually but if you edit a front-end file in the Node-RED Editor uibuilder node, you can tell your front-end to automatically reload on saving changes.

However, there are still potential advantages to using a dev server. The nice thing about the Svelte dev server is that it doesn't make you make any changes to your front-end code to allow uibuilder to work. Other dev servers require you to adjust the URL's and start function parameters but for some reason Svelte does not.

It really isn't an underlying problem, dev servers are meant for development and you don't normally expect to reboot your dev server device when developing.

uibuilder should be working off the Node-RED (or uibuilder custom) web server (or via a reverse proxy). Only use the dev server version when making rapid changes to the front-end code. In that case, you will not see this warning.

How would you expect that to appear in the display? a HEX representation?

One interesting thing about MQTT v5 is that you can provide a mime-type for the value and that would be an interesting extension.

I have to say though, I don't know whether I'll get time to work on this further at least in the short-term. I got the relatively easy bits done but going further is likely a fair bit of work. I'd like to but time is limited.

I'll find some time to create a repository though so other people can also work on it if they want to.

1 Like

Initial thought would be same visually as debug shows a buffer
e.g
image

I can't spull hux :slight_smile:

1 Like