From c83b90f4f8dfcef9c7341f89450661cd6871aa91 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 30 Nov 2025 20:13:41 -0600 Subject: [PATCH] Enhance Network Visualiser UI and functionality - Redesigned control panel for better accessibility and aesthetics. - Added loading state for update button and improved tooltip styles. - Introduced conversation fetching and icon generation for user nodes. - Updated node and edge styling for improved visibility and user experience. - Enhanced sidebar search functionality to display dynamic placeholder text. --- .../network-visualiser/NetworkVisualiser.vue | 225 +++++++++++++----- .../nomadnetwork/NomadNetworkSidebar.vue | 2 +- 2 files changed, 173 insertions(+), 54 deletions(-) diff --git a/src/frontend/components/network-visualiser/NetworkVisualiser.vue b/src/frontend/components/network-visualiser/NetworkVisualiser.vue index 2d3512e..621e4b2 100644 --- a/src/frontend/components/network-visualiser/NetworkVisualiser.vue +++ b/src/frontend/components/network-visualiser/NetworkVisualiser.vue @@ -3,38 +3,41 @@
-
-
-
-
Reticulum Network
-
- -
+
+
+
+
Reticulum Network
+
-
-
-
-
- -
- -
+
+
+ +
-
-
Interfaces
-
{{ onlineInterfaces.length }} Online, {{ offlineInterfaces.length }} Offline
+
+
Interfaces
+
+ {{ onlineInterfaces.length }} Online, + {{ offlineInterfaces.length }} Offline +
@@ -45,7 +48,24 @@ @@ -53,6 +73,7 @@ import "vis-network/styles/vis-network.css"; import { Network } from "vis-network"; import { DataSet } from "vis-data"; +import * as mdi from "@mdi/js"; import Utils from "../../js/Utils"; export default { name: 'NetworkVisualiser', @@ -62,12 +83,15 @@ export default { autoReload: false, reloadInterval: null, isShowingControls: true, + isUpdating: false, interfaces: [], pathTable: [], announces: {}, + conversations: {}, network: null, nodes: new DataSet(), edges: new DataSet(), + iconCache: {}, }; }, beforeUnmount() { @@ -118,6 +142,70 @@ export default { console.log(e); } }, + async getConversations() { + try { + const response = await window.axios.get(`/api/v1/lxmf/conversations`); + this.conversations = {}; + for(const conversation of response.data.conversations){ + this.conversations[conversation.destination_hash] = conversation; + } + } catch(e) { + console.log(e); + } + }, + async createIconImage(iconName, foregroundColor, backgroundColor, size = 32) { + const cacheKey = `${iconName}-${foregroundColor}-${backgroundColor}-${size}`; + if(this.iconCache[cacheKey]){ + return this.iconCache[cacheKey]; + } + + return new Promise((resolve) => { + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + + // draw background circle + ctx.fillStyle = backgroundColor; + ctx.beginPath(); + ctx.arc(size / 2, size / 2, size / 2 - 1, 0, 2 * Math.PI); + ctx.fill(); + + // load MDI icon SVG + const iconSvg = this.getMdiIconSvg(iconName, foregroundColor); + const img = new Image(); + const svgBlob = new Blob([iconSvg], { type: 'image/svg+xml' }); + const url = URL.createObjectURL(svgBlob); + img.onload = () => { + ctx.drawImage(img, size * 0.2, size * 0.2, size * 0.6, size * 0.6); + URL.revokeObjectURL(url); + const dataUrl = canvas.toDataURL(); + this.iconCache[cacheKey] = dataUrl; + resolve(dataUrl); + }; + img.onerror = () => { + URL.revokeObjectURL(url); + const dataUrl = canvas.toDataURL(); + this.iconCache[cacheKey] = dataUrl; + resolve(dataUrl); + }; + img.src = url; + }); + }, + getMdiIconSvg(iconName, foregroundColor) { + const mdiIconName = "mdi" + iconName.split("-").map((word) => { + return word.charAt(0).toUpperCase() + word.slice(1); + }).join(""); + + const iconPath = mdi[mdiIconName] || mdi["mdiAccountOutline"]; + + return ``; + }, + async createMdiIconImage(iconName, size = 32) { + const foregroundColor = '#ffffff'; + const backgroundColor = '#6b7280'; + return await this.createIconImage(iconName, foregroundColor, backgroundColor, size); + }, async init() { // create network ui @@ -135,11 +223,23 @@ export default { }, nodes: { color: { - border: "#000000", + border: "#e5e7eb", highlight: { - border: "#000000", + border: "#3b82f6", }, }, + font: { + color: "#111827", + size: 14, + background: "rgba(255, 255, 255, 0.9)", + }, + }, + edges: { + color: { + color: "#9ca3af", + highlight: "#3b82f6", + }, + width: 2, }, physics: { barnesHut: { @@ -256,14 +356,24 @@ export default { }, async update() { - await this.getConfig(); - await this.getInterfaceStats(); - await this.getPathTable(); - await this.getAnnounces(); + this.isUpdating = true; + try { + await this.getConfig(); + await this.getInterfaceStats(); + await this.getPathTable(); + await this.getAnnounces(); + await this.getConversations(); + } finally { + this.isUpdating = false; + } const nodes = []; const edges = []; + const isDarkMode = document.documentElement.classList.contains('dark'); + const fontColor = isDarkMode ? "#f4f4f5" : "#111827"; + const fontBackground = isDarkMode ? "rgba(24, 24, 27, 0.9)" : "rgba(255, 255, 255, 0.9)"; + // add me nodes.push({ id: "me", @@ -275,8 +385,8 @@ export default { `Identity: ${this.config?.identity_hash ?? 'Unknown'}`, ].join("\n"), font: { - color: "#000000", - background: "#ffffff", + color: fontColor, + background: fontBackground, }, }); @@ -305,8 +415,8 @@ export default { ].join("\n"), size: 30, font: { - color: "#000000", - background: '#ffffff', + color: fontColor, + background: fontBackground, }, shape: "circularImage", image: entry.status ? "/assets/images/network-visualiser/interface_connected.png" : "/assets/images/network-visualiser/interface_disconnected.png", @@ -322,12 +432,9 @@ export default { id: `${entry.parent_interface_name}~${entry.name}`, from: entry.parent_interface_name, to: entry.name, - color: "transparent", + color: entry.status ? "#22c55e" : "#ef4444", + width: 3, length: 300, - background: { - enabled: true, - color: entry.status ? "#22c55e" : "#ef4444", - }, }); } else { // add edge from me to interface @@ -335,12 +442,9 @@ export default { id: `me~${entry.name}`, from: "me", to: entry.name, - color: "transparent", + color: entry.status ? "#22c55e" : "#ef4444", + width: 3, length: 300, - background: { - enabled: true, - color: entry.status ? "#22c55e" : "#ef4444", - }, }); } @@ -375,9 +479,22 @@ export default { if(announce.aspect === "lxmf.delivery"){ const name = announce.custom_display_name ?? announce.display_name; + const conversation = this.conversations[announce.destination_hash]; node.shape = "circularImage"; - node.image = entry.hops === 1 ? "/assets/images/network-visualiser/user_1hop.png" : "/assets/images/network-visualiser/user.png"; + + if(conversation?.lxmf_user_icon){ + const iconImage = await this.createIconImage( + conversation.lxmf_user_icon.icon_name, + conversation.lxmf_user_icon.foreground_colour, + conversation.lxmf_user_icon.background_colour, + 40 + ); + node.image = iconImage; + node.size = 30; + } else { + node.image = entry.hops === 1 ? "/assets/images/network-visualiser/user_1hop.png" : "/assets/images/network-visualiser/user.png"; + } node.label = name; node.title = [ @@ -423,7 +540,8 @@ export default { id: `${entry.interface}~${entry.hash}`, from: entry.interface, to: entry.hash, - color: "gray", + color: isDarkMode ? "#71717a" : "#9ca3af", + width: 2, }); } @@ -504,3 +622,4 @@ export default { }, } + diff --git a/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue b/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue index 0ffecf9..9f59458 100644 --- a/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue +++ b/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue @@ -12,7 +12,7 @@
- +