reticulum-meshchat/src/frontend/components/network-visualiser/NetworkVisualiser.vue
2024-08-12 00:39:26 +00:00

313 lines
10 KiB
Vue

<template>
<div class="flex-1 h-full min-w-full sm:min-w-[500px] relative">
<!-- network -->
<div id="network" class="w-full h-full"></div>
<!-- loading -->
<div v-if="isLoading" class="absolute flex top-0 bottom-0 left-0 right-0">
<div class="mx-auto my-auto">
<svg class="animate-spin h-6 w-6 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
</div>
</div>
</template>
<style>
.vis-tooltip {
color: white !important;
background: rgba(0, 0, 0, 0.75) !important;
}
</style>
<script>
import "vis-network/styles/vis-network.css";
import { Network } from "vis-network";
import Utils from "../../js/Utils";
export default {
name: 'NetworkVisualiser',
data() {
return {
config: null,
isLoading: false,
interfaces: [],
pathTable: [],
announces: {},
};
},
mounted() {
this.init();
},
methods: {
async getInterfaceStats() {
try {
const response = await axios.get(`/api/v1/interface-stats`);
this.interfaces = response.data.interface_stats?.interfaces ?? [];
} catch(e) {
alert("failed to load interface stats");
console.log(e);
}
},
async getPathTable() {
try {
const response = await axios.get(`/api/v1/path-table`);
this.pathTable = response.data.path_table;
} catch(e) {
alert("failed to load path table");
console.log(e);
}
},
async getConfig() {
try {
const response = await axios.get("/api/v1/config");
this.config = response.data.config;
} catch(e) {
alert("failed to load config");
console.error(e);
}
},
async getAnnounces() {
try {
// fetch announces
const response = await window.axios.get(`/api/v1/announces`);
// cache announces
this.announces = {};
for(const announce of response.data.announces){
this.announces[announce.destination_hash] = announce;
}
} catch(e) {
// do nothing if failed to load announces
console.log(e);
}
},
async init() {
this.isLoading = true;
await this.update();
},
async update() {
await this.getConfig();
await this.getInterfaceStats();
await this.getPathTable();
await this.getAnnounces();
const nodes = [];
const edges = [];
// add me
nodes.push({
id: "me",
group: "me",
size: 60,
label: this.config?.display_name ?? "This Device",
title: [
`${this.config?.display_name ?? 'This Device'}`,
`Identity: ${this.config?.identity_hash ?? 'Unknown'}`,
].join("\n"),
font: {
color: "#000000",
background: "#ffffff",
},
});
// add interfaces
for(const entry of this.interfaces){
// determine label
var label = entry.interface_name ?? entry.name;
// we want to show the full info for LocalServerInterface
// we also want to show the full info instead of just "Client on Name" for TCPServerInterface clients
if(entry.type === "LocalServerInterface" || entry.parent_interface_name != null){
label = entry.name;
}
const node = {
id: entry.name,
group: "interface",
label: label,
title: [
entry.name,
`State: ${entry.status ? 'Online' : 'Offline'}`,
`Bitrate: ${this.formatBitsPerSecond(entry.bitrate)}`,
`TX: ${this.formatBytes(entry.txb)}`,
`RX: ${this.formatBytes(entry.rxb)}`,
].join("\n"),
size: 30,
font: {
color: "#000000",
background: '#ffffff',
},
shape: "circularImage",
image: entry.status ? "/assets/images/network-visualiser/interface_connected.png" : "/assets/images/network-visualiser/interface_disconnected.png",
};
// add interface node
nodes.push(node);
// add edge to interface
if(entry.parent_interface_name){
// add edge from parent interface to interface
edges.push({
from: entry.parent_interface_name,
to: entry.name,
color: "transparent",
length: 300,
background: {
enabled: true,
color: entry.status ? "#22c55e" : "#ef4444",
},
});
} else {
// add edge from me to interface
edges.push({
from: "me",
to: entry.name,
color: "transparent",
length: 300,
background: {
enabled: true,
color: entry.status ? "#22c55e" : "#ef4444",
},
});
}
}
// add paths for announces
for(const entry of this.pathTable){
// skip this path if hops are unknown
if(entry.hops == null){
continue;
}
// find what announced this path, or skip showing it for now
const announce = this.announces[entry.hash];
if(!announce){
continue;
}
const node = {
id: entry.hash,
group: "announce",
size: 20,
};
if(announce.aspect === "lxmf.delivery"){
const name = announce.app_data ? atob(announce.app_data) : "Anonymous Peer";
node.shape = "circularImage";
node.image = "/assets/images/network-visualiser/user.png";
node.label = name;
node.title = [
`Name: ${name}`,
`Aspect: ${announce.aspect}`,
`Identity: ${announce.identity_hash}`,
`Destination: ${announce.destination_hash}`,
`Path: ${entry.hops} ${entry.hops === 1 ? 'Hop' : 'Hops'} via ${entry.interface}`,
`Announced At: ${announce.updated_at}`,
].join("\n");
}
if(announce.aspect === "nomadnetwork.node"){
const name = announce.app_data ? atob(announce.app_data) : "Anonymous Node";
node.shape = "circularImage";
node.image = "/assets/images/network-visualiser/server.png";
node.label = name;
node.title = [
`Name: ${name}`,
`Aspect: ${announce.aspect}`,
`Identity: ${announce.identity_hash}`,
`Destination: ${announce.destination_hash}`,
`Path: ${entry.hops} ${entry.hops === 1 ? 'Hop' : 'Hops'} via ${entry.interface}`,
`Announced At: ${announce.updated_at}`,
].join("\n");
}
// add node
nodes.push(node);
// add edge from interface to announced aspect
edges.push({
from: entry.interface,
to: entry.hash,
color: "gray",
});
}
// create network ui
const container = document.getElementById("network");
const network = new Network(container, {
nodes: nodes,
edges: edges,
}, {
interaction: {
tooltipDelay: 0, // show tooltip instantly on hover
},
layout: {
// always layout nodes the same way across reloads if nothing changed
randomSeed: 1,
},
nodes: {
color: {
border: "#000000",
highlight: {
border: "#000000",
},
},
},
physics: {
barnesHut: {
gravitationalConstant: -5000,
centralGravity: 0,
springConstant: 0.1,
damping: 0.15,
},
maxVelocity: 150,
minVelocity: 0.25,
},
groups: {
"me": {
shape: "image",
image: "/assets/images/reticulum_logo_512.png",
},
"interface": {
},
"announce": {
},
},
});
// hide loading once done
network.on("stabilized", () => {
this.isLoading = false;
});
},
formatBytes: function(bytes) {
return Utils.formatBytes(bytes);
},
formatBitsPerSecond: function(bits) {
return Utils.formatBitsPerSecond(bits);
},
},
}
</script>