Removed dashboard because I hate it
This commit is contained in:
parent
b3ff6f069c
commit
8f615e179e
187
dashboard.html
187
dashboard.html
@ -1,187 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>MQTT Device Dashboard (Nested Flash)</title>
|
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
||||||
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
|
|
||||||
<style>
|
|
||||||
body { font-family: sans-serif; margin: 1rem; }
|
|
||||||
.device { border: 1px solid #ccc; padding: 0.5rem; margin: 0.5rem; cursor: pointer; }
|
|
||||||
.selected { background: #eef; }
|
|
||||||
.panel { display: flex; gap: 1rem; }
|
|
||||||
.left, .right { flex: 1; }
|
|
||||||
.tree ul { list-style: none; padding-left: 1rem; margin: 0; }
|
|
||||||
.tree li { margin: 2px 0; }
|
|
||||||
.tree span { cursor: pointer; user-select: none; }
|
|
||||||
.flash {
|
|
||||||
animation: flash-bg 1s ease;
|
|
||||||
}
|
|
||||||
@keyframes flash-bg {
|
|
||||||
0% { background-color: yellow; }
|
|
||||||
100% { background-color: transparent; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="app">
|
|
||||||
<h1>Device Dashboard</h1>
|
|
||||||
<div class="panel">
|
|
||||||
<div class="left">
|
|
||||||
<h2>Devices</h2>
|
|
||||||
<div v-for="(device, id) in devices"
|
|
||||||
:key="id"
|
|
||||||
@click="selected = id"
|
|
||||||
:class="['device', {selected: selected===id}]">
|
|
||||||
{{ id }} — {{ device.status }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="right" v-if="selected">
|
|
||||||
<h2>Properties & Commands: {{ selected }}</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3>Properties</h3>
|
|
||||||
<div class="tree">
|
|
||||||
<tree-view :data="devices[selected].properties"
|
|
||||||
:flash-map="flashMap"></tree-view>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3>Commands</h3>
|
|
||||||
<ul>
|
|
||||||
<li v-for="(cmd, name) in devices[selected].commands" :key="name">
|
|
||||||
{{ name }} — schema: {{ JSON.stringify(cmd.schema) }}
|
|
||||||
<button @click="sendCommand(selected, name)">Send</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const { createApp } = Vue;
|
|
||||||
|
|
||||||
// Recursive tree component
|
|
||||||
const TreeView = {
|
|
||||||
name: "TreeView",
|
|
||||||
props: ["data", "flashMap", "basePath"],
|
|
||||||
data() { return { openNodes: new Set() }; },
|
|
||||||
methods: {
|
|
||||||
toggle(key) {
|
|
||||||
if (this.openNodes.has(key)) this.openNodes.delete(key);
|
|
||||||
else this.openNodes.add(key);
|
|
||||||
},
|
|
||||||
isObject(val) { return val && typeof val === "object" && !Array.isArray(val); },
|
|
||||||
getFlashClass(fullPath) {
|
|
||||||
return this.flashMap[fullPath] ? "flash" : "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
template: `
|
|
||||||
<ul>
|
|
||||||
<li v-for="(val, key) in data" :key="key">
|
|
||||||
<span v-if="isObject(val)"
|
|
||||||
@click="toggle(key)"
|
|
||||||
:class="getFlashClass(basePath ? basePath + '.' + key : key)">
|
|
||||||
{{ key }} {{ openNodes.has(key) ? '▼' : '▶' }}
|
|
||||||
</span>
|
|
||||||
<span v-else :class="getFlashClass(basePath ? basePath + '.' + key : key)">
|
|
||||||
{{ key }}: {{ val }}
|
|
||||||
</span>
|
|
||||||
<tree-view
|
|
||||||
v-if="isObject(val) && openNodes.has(key)"
|
|
||||||
:data="val"
|
|
||||||
:flash-map="flashMap"
|
|
||||||
:base-path="basePath ? basePath + '.' + key : key">
|
|
||||||
</tree-view>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`,
|
|
||||||
components: { TreeView: null },
|
|
||||||
created() { this.$options.components.TreeView = TreeView; }
|
|
||||||
};
|
|
||||||
|
|
||||||
createApp({
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
devices: {},
|
|
||||||
selected: null,
|
|
||||||
client: null,
|
|
||||||
flashMap: {} // maps "full.path" => boolean
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const clientID = "webui-" + Math.random().toString(16).substr(2, 8);
|
|
||||||
this.client = mqtt.connect("ws://localhost:8083/mqtt", { clientId: clientID });
|
|
||||||
|
|
||||||
this.client.on("connect", () => {
|
|
||||||
console.log("Connected to broker");
|
|
||||||
this.client.subscribe("asset/+/status");
|
|
||||||
this.client.subscribe("asset/+/property/#");
|
|
||||||
this.client.subscribe("asset/+/command/+");
|
|
||||||
});
|
|
||||||
|
|
||||||
this.client.on("message", (topic, payloadBuffer) => {
|
|
||||||
const payload = payloadBuffer.toString();
|
|
||||||
const parts = topic.split("/");
|
|
||||||
|
|
||||||
if (parts[0] !== "asset") return;
|
|
||||||
const deviceID = parts[1];
|
|
||||||
const category = parts[2];
|
|
||||||
const keyPath = parts.slice(3);
|
|
||||||
|
|
||||||
if (!this.devices[deviceID]) {
|
|
||||||
this.devices[deviceID] = { status: "OFFLINE", properties: {}, commands: {} };
|
|
||||||
}
|
|
||||||
const device = this.devices[deviceID];
|
|
||||||
|
|
||||||
if (category === "status") {
|
|
||||||
device.status = payload;
|
|
||||||
} else if (category === "property") {
|
|
||||||
let target = device.properties;
|
|
||||||
for (let i = 0; i < keyPath.length - 1; i++) {
|
|
||||||
const k = keyPath[i];
|
|
||||||
if (!target[k] || typeof target[k] !== "object") target[k] = {};
|
|
||||||
target = target[k];
|
|
||||||
}
|
|
||||||
|
|
||||||
const leafKey = keyPath[keyPath.length - 1];
|
|
||||||
let value;
|
|
||||||
try { value = JSON.parse(payload); }
|
|
||||||
catch(e) { value = payload; }
|
|
||||||
|
|
||||||
target[leafKey] = value;
|
|
||||||
|
|
||||||
// generate all ancestor paths for flashing
|
|
||||||
const paths = [];
|
|
||||||
for (let i = 0; i < keyPath.length; i++) {
|
|
||||||
paths.push(keyPath.slice(0, i + 1).join("."));
|
|
||||||
}
|
|
||||||
paths.forEach(p => {
|
|
||||||
this.flashMap[p] = true;
|
|
||||||
setTimeout(() => { this.flashMap[p] = false; }, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
} else if (category === "command" && keyPath.length === 1) {
|
|
||||||
const cmdName = keyPath[0];
|
|
||||||
try { device.commands[cmdName] = JSON.parse(payload); }
|
|
||||||
catch(e) { device.commands[cmdName] = { raw: payload }; }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
sendCommand(deviceID, commandName) {
|
|
||||||
const topic = `asset/${deviceID}/command/${commandName}`;
|
|
||||||
const payload = JSON.stringify({ args: {} });
|
|
||||||
this.client.publish(topic, payload);
|
|
||||||
console.log("Sent command", topic, payload);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: { TreeView }
|
|
||||||
}).mount("#app");
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Loading…
Reference in New Issue
Block a user