From 0f759cbe5c440c773d54ce4583091abe20fee738 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Mon, 8 Jul 2024 15:48:22 -0500 Subject: [PATCH] Initial try at a tree view. Includes a few formatting options that will be useful elsewhere. --- .../src/node_manager/js_build/esbuild.sh | 2 +- .../js_build/src/helpers/scaling.js | 49 +++ .../src/node_manager/js_build/src/tree.js | 292 ++++++++++++++++++ src/rust/lqosd/src/node_manager/local_api.rs | 2 + .../node_manager/local_api/network_tree.rs | 20 ++ .../src/node_manager/static2/node_manager.css | 20 +- .../src/node_manager/static2/template.html | 2 +- .../lqosd/src/node_manager/static2/tree.html | 29 ++ .../lqosd/src/node_manager/static_pages.rs | 2 +- .../src/node_manager/ws/published_channels.rs | 1 + src/rust/lqosd/src/node_manager/ws/ticker.rs | 2 + .../node_manager/ws/ticker/network_tree.rs | 31 ++ 12 files changed, 448 insertions(+), 4 deletions(-) create mode 100644 src/rust/lqosd/src/node_manager/js_build/src/tree.js create mode 100644 src/rust/lqosd/src/node_manager/local_api/network_tree.rs create mode 100644 src/rust/lqosd/src/node_manager/static2/tree.html create mode 100644 src/rust/lqosd/src/node_manager/ws/ticker/network_tree.rs diff --git a/src/rust/lqosd/src/node_manager/js_build/esbuild.sh b/src/rust/lqosd/src/node_manager/js_build/esbuild.sh index 4919bd06..d9785216 100755 --- a/src/rust/lqosd/src/node_manager/js_build/esbuild.sh +++ b/src/rust/lqosd/src/node_manager/js_build/esbuild.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -scripts=( index.js template.js login.js first-run.js shaped-devices.js ) +scripts=( index.js template.js login.js first-run.js shaped-devices.js tree.js ) for script in "${scripts[@]}" do echo "Building {$script}" diff --git a/src/rust/lqosd/src/node_manager/js_build/src/helpers/scaling.js b/src/rust/lqosd/src/node_manager/js_build/src/helpers/scaling.js index 041e6c1e..79efb8a5 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/helpers/scaling.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/helpers/scaling.js @@ -52,4 +52,53 @@ export function lerpGreenToRedViaOrange(value, max) { g = 255; } return `rgb(${r}, ${g}, ${b})`; +} + +export function formatThroughput(throughput, limitInMbps) { + let limitBits = limitInMbps * 1000 * 1000; + let percent = 0; + if (limitBits > 0) { + percent = (throughput / limitBits) * 100; + } + let blob = ""; + blob += ""; + for (let i=0; i<100; i+=10) { + let color = lerpGreenToRedViaOrange(100-i, 100); + if (percent < i) { + blob += "░"; + } else { + blob += ""; + } + } + blob += ""; + + blob += "" + scaleNumber(throughput * 8, 1) + "bps"; + blob += ""; + return blob; +} + +export function formatRtt(rtt) { + if (rtt === undefined) { + return "-"; + } + const limit = 200; + let percent = 0; + if (limit > 0) { + percent = (rtt / limit) * 100; + } + let blob = ""; + blob += ""; + for (let i=0; i<100; i+=10) { + let color = lerpGreenToRedViaOrange(100-i, 100); + if (percent < i) { + blob += "░"; + } else { + blob += ""; + } + } + blob += ""; + + blob += "" + parseFloat(rtt).toFixed(0) + " ms"; + blob += ""; + return blob; } \ No newline at end of file diff --git a/src/rust/lqosd/src/node_manager/js_build/src/tree.js b/src/rust/lqosd/src/node_manager/js_build/src/tree.js new file mode 100644 index 00000000..38a5345b --- /dev/null +++ b/src/rust/lqosd/src/node_manager/js_build/src/tree.js @@ -0,0 +1,292 @@ +import {clearDiv, theading} from "./helpers/builders"; +import {formatRtt, formatThroughput, lerpGreenToRedViaOrange, scaleNumber} from "./helpers/scaling"; +import {subscribeWS} from "./pubsub/ws"; + +var tree = null; +var parent = 0; + +// This runs first and builds the initial structure on the page +function getInitialTree() { + $.get("/local-api/networkTree/0", (data) => { + //console.log(data); + tree = data; + + let treeTable = document.createElement("table"); + treeTable.classList.add("table", "table-striped", "table-bordered"); + let thead = document.createElement("thead"); + thead.appendChild(theading("Name")); + thead.appendChild(theading("Limit")); + thead.appendChild(theading("⬇️")); + thead.appendChild(theading("⬆️")); + thead.appendChild(theading("RTT ⬇️")); + thead.appendChild(theading("RTT ⬆️")); + thead.appendChild(theading("Re-xmit ⬇️")); + thead.appendChild(theading("Re-xmit ⬆️")); + thead.appendChild(theading("ECN ⬇️")); + thead.appendChild(theading("ECN ⬆️")); + thead.appendChild(theading("Drops ⬇️")); + thead.appendChild(theading("Drops ⬆️")); + + treeTable.appendChild(thead); + let tbody = document.createElement("tbody"); + for (let i=0; i 0) { + nodeName += "└"; + } + for (let j=1; j 0) nodeName += " "; + nodeName += ""; + nodeName += node.name; + nodeName += ""; + if (node.type !== null) { + nodeName += " (" + node.type + ")"; + } + col.innerHTML = nodeName; + col.classList.add("small", "redactable"); + row.appendChild(col); + + col = document.createElement("td"); + col.id = "limit-" + nodeId; + col.classList.add("small"); + let limit = ""; + if (node.max_throughput[0] === 0) { + limit = "Unlimited"; + } else { + limit = scaleNumber(node.max_throughput[0] * 1000 * 1000, 0); + } + limit += " / "; + if (node.max_throughput[1] === 0) { + limit += "Unlimited"; + } else { + limit += scaleNumber(node.max_throughput[0] * 1000 * 1000, 0); + } + col.textContent = limit; + row.appendChild(col); + + col = document.createElement("td"); + col.id = "down-" + nodeId; + col.classList.add("small"); + col.innerHTML = formatThroughput(node.current_throughput[0] * 8, node.max_throughput[0]); + row.appendChild(col); + + col = document.createElement("td"); + col.id = "up-" + nodeId; + col.classList.add("small"); + col.innerHTML = formatThroughput(node.current_throughput[1] * 8, node.max_throughput[1]); + row.appendChild(col); + + col = document.createElement("td"); + col.id = "rtt-down-" + nodeId; + col.innerHTML = formatRtt(node.rtts[0]); + row.appendChild(col); + + col = document.createElement("td"); + col.id = "rtt-up-" + nodeId; + col.innerHTML = formatRtt(node.rtts[1]); + row.appendChild(col); + + col = document.createElement("td"); + col.id = "re-xmit-down-" + nodeId; + if (node.current_retransmits[0] !== undefined) { + col.textContent = node.current_retransmits[0]; + } else { + col.textContent = "-"; + } + row.appendChild(col); + + col = document.createElement("td"); + col.id = "re-xmit-up-" + nodeId; + if (node.current_retransmits[1] !== undefined) { + col.textContent = node.current_retransmits[1]; + } else { + col.textContent = "-"; + } + row.appendChild(col); + + col = document.createElement("td"); + col.id = "ecn-down-" + nodeId; + if (node.current_marks[0] !== undefined) { + col.textContent = node.current_marks[0]; + } else { + col.textContent = "-"; + } + row.appendChild(col); + + col = document.createElement("td"); + col.id = "ecn-up-" + nodeId; + if (node.current_marks[1] !== undefined) { + col.textContent = node.current_marks[1]; + } else { + col.textContent = "-"; + } + row.appendChild(col); + + col = document.createElement("td"); + col.id = "drops-down-" + nodeId; + if (node.current_drops[0] !== undefined) { + col.textContent = node.current_drops[0]; + } else { + col.textContent = "-"; + } + row.appendChild(col); + + col = document.createElement("td"); + col.id = "drops-up-" + nodeId; + if (node.current_drops[1] !== undefined) { + col.textContent = node.current_drops[1]; + } else { + col.textContent = "-"; + } + row.appendChild(col); + + return row; +} + +function onMessage(msg) { + if (msg.event === "NetworkTree") { + //console.log(msg); + msg.data.forEach((n) => { + let nodeId = n[0]; + let node = n[1]; + + let col = document.getElementById("down-" + nodeId); + if (col !== null) { + col.innerHTML = formatThroughput(node.current_throughput[0] * 8, node.max_throughput[0]); + } + col = document.getElementById("up-" + nodeId); + if (col !== null) { + col.innerHTML = formatThroughput(node.current_throughput[1] * 8, node.max_throughput[1]); + } + col = document.getElementById("rtt-down-" + nodeId); + if (col !== null) { + col.innerHTML = formatRtt(node.rtts[0]); + } + col = document.getElementById("rtt-up-" + nodeId); + if (col !== null) { + col.innerHTML = formatRtt(node.rtts[1]); + } + col = document.getElementById("re-xmit-down-" + nodeId); + if (col !== null) { + if (node.current_retransmits[0] !== undefined) { + col.textContent = node.current_retransmits[0]; + } else { + col.textContent = "-"; + } + } + col = document.getElementById("re-xmit-up-" + nodeId); + if (col !== null) { + if (node.current_retransmits[1] !== undefined) { + col.textContent = node.current_retransmits[1]; + } else { + col.textContent = "-"; + } + } + col = document.getElementById("ecn-down-" + nodeId); + if (col !== null) { + if (node.current_marks[0] !== undefined) { + col.textContent = node.current_marks[0]; + } else { + col.textContent = "-"; + } + } + col = document.getElementById("ecn-up-" + nodeId); + if (col !== null) { + if (node.current_marks[1] !== undefined) { + col.textContent = node.current_marks[1]; + } else { + col.textContent = "-"; + } + } + col = document.getElementById("drops-down-" + nodeId); + if (col !== null) { + if (node.current_drops[0] !== undefined) { + col.textContent = node.current_drops[0]; + } else { + col.textContent = "-"; + } + } + col = document.getElementById("drops-up-" + nodeId); + if (col !== null) { + if (node.current_drops[1] !== undefined) { + col.textContent = node.current_drops[1]; + } else { + col.textContent = "-"; + } + } + }); + } +} + +const params = new Proxy(new URLSearchParams(window.location.search), { + get: (searchParams, prop) => searchParams.get(prop), +}); + +if (params.parent !== null) { + parent = parseInt(params.parent); +} else { + parent = 0; +} + +getInitialTree(); diff --git a/src/rust/lqosd/src/node_manager/local_api.rs b/src/rust/lqosd/src/node_manager/local_api.rs index a5cb3d44..942e371c 100644 --- a/src/rust/lqosd/src/node_manager/local_api.rs +++ b/src/rust/lqosd/src/node_manager/local_api.rs @@ -2,6 +2,7 @@ mod dashboard_themes; mod version_check; mod device_counts; mod shaped_device_api; +mod network_tree; use axum::Router; use axum::routing::{get, post}; @@ -15,4 +16,5 @@ pub fn local_api() -> Router { .route("/versionCheck", get(version_check::version_check)) .route("/deviceCount", get(device_counts::count_users)) .route("/devicesAll", get(shaped_device_api::all_shaped_devices)) + .route("/networkTree/:parent", get(network_tree::get_network_tree)) } \ No newline at end of file diff --git a/src/rust/lqosd/src/node_manager/local_api/network_tree.rs b/src/rust/lqosd/src/node_manager/local_api/network_tree.rs new file mode 100644 index 00000000..763c5523 --- /dev/null +++ b/src/rust/lqosd/src/node_manager/local_api/network_tree.rs @@ -0,0 +1,20 @@ +use axum::extract::Path; +use axum::Json; +use lqos_bus::BusResponse; +use lqos_config::NetworkJsonTransport; +use crate::shaped_devices_tracker; +use crate::shaped_devices_tracker::NETWORK_JSON; + +pub async fn get_network_tree( + Path(parent): Path +) -> Json> { + let net_json = NETWORK_JSON.read().unwrap(); + let result: Vec<(usize, NetworkJsonTransport)> = net_json + .nodes + .iter() + .enumerate() + .map(|(i, n) | (i, n.clone_to_transit())) + .collect(); + + Json(result) +} \ No newline at end of file diff --git a/src/rust/lqosd/src/node_manager/static2/node_manager.css b/src/rust/lqosd/src/node_manager/static2/node_manager.css index 28276fdb..b65ff247 100644 --- a/src/rust/lqosd/src/node_manager/static2/node_manager.css +++ b/src/rust/lqosd/src/node_manager/static2/node_manager.css @@ -79,4 +79,22 @@ body.dark-mode { min-height: 500px; } .dashEditButton { } -.redactable { } \ No newline at end of file +.redactable { } +.small { font-size: 8pt; } + +/* Funky tricks for the tree view */ +.overlayThroughputWrapper { + position: relative; + width: 100%; + height: auto; + text-align: center; +} +.overlayThroughputNumber { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 1.2em; + color: white; + text-shadow: 2px 2px 4px #000000; +} \ No newline at end of file diff --git a/src/rust/lqosd/src/node_manager/static2/template.html b/src/rust/lqosd/src/node_manager/static2/template.html index fa839d30..2215bc77 100644 --- a/src/rust/lqosd/src/node_manager/static2/template.html +++ b/src/rust/lqosd/src/node_manager/static2/template.html @@ -49,7 +49,7 @@ diff --git a/src/rust/lqosd/src/node_manager/static2/tree.html b/src/rust/lqosd/src/node_manager/static2/tree.html new file mode 100644 index 00000000..16f44997 --- /dev/null +++ b/src/rust/lqosd/src/node_manager/static2/tree.html @@ -0,0 +1,29 @@ +
+
+
Network Tree ()
+ + + + + + + + + + + + + + + + +
Limits:
Throughput:
RTT:
+
+
+
+
+

Loading, Please Wait

+
+
+ + \ No newline at end of file diff --git a/src/rust/lqosd/src/node_manager/static_pages.rs b/src/rust/lqosd/src/node_manager/static_pages.rs index 829c3c97..53f8fad1 100644 --- a/src/rust/lqosd/src/node_manager/static_pages.rs +++ b/src/rust/lqosd/src/node_manager/static_pages.rs @@ -29,7 +29,7 @@ pub(super) fn static_routes() -> Result { // Add HTML pages to serve directly to this list, otherwise // they won't have template + authentication applied to them. let html_pages = [ - "index.html", "shaped_devices.html" + "index.html", "shaped_devices.html", "tree.html" ]; // Iterate through pages and construct the router diff --git a/src/rust/lqosd/src/node_manager/ws/published_channels.rs b/src/rust/lqosd/src/node_manager/ws/published_channels.rs index d1c78388..d0f4d677 100644 --- a/src/rust/lqosd/src/node_manager/ws/published_channels.rs +++ b/src/rust/lqosd/src/node_manager/ws/published_channels.rs @@ -19,4 +19,5 @@ pub enum PublishedChannels { Ram, TreeSummary, QueueStatsTotal, + NetworkTree, } diff --git a/src/rust/lqosd/src/node_manager/ws/ticker.rs b/src/rust/lqosd/src/node_manager/ws/ticker.rs index efa78e3a..4e84c1b1 100644 --- a/src/rust/lqosd/src/node_manager/ws/ticker.rs +++ b/src/rust/lqosd/src/node_manager/ws/ticker.rs @@ -9,6 +9,7 @@ mod flow_endpoints; pub mod system_info; mod tree_summary; mod queue_stats_total; +mod network_tree; use std::sync::Arc; use crate::node_manager::ws::publish_subscribe::PubSub; @@ -36,6 +37,7 @@ pub(super) async fn channel_ticker(channels: Arc) { system_info::ram_info(channels.clone()), tree_summary::tree_summary(channels.clone()), queue_stats_total::queue_stats_totals(channels.clone()), + network_tree::network_tree(channels.clone()), ); } } \ No newline at end of file diff --git a/src/rust/lqosd/src/node_manager/ws/ticker/network_tree.rs b/src/rust/lqosd/src/node_manager/ws/ticker/network_tree.rs new file mode 100644 index 00000000..a3f59e2a --- /dev/null +++ b/src/rust/lqosd/src/node_manager/ws/ticker/network_tree.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; +use serde_json::json; +use tokio::task::spawn_blocking; +use lqos_config::NetworkJsonTransport; +use crate::node_manager::ws::publish_subscribe::PubSub; +use crate::node_manager::ws::published_channels::PublishedChannels; +use crate::shaped_devices_tracker::NETWORK_JSON; + +pub async fn network_tree(channels: Arc) { + if !channels.is_channel_alive(PublishedChannels::NetworkTree).await { + return; + } + + let data: Vec<(usize, NetworkJsonTransport)> = spawn_blocking(|| { + let net_json = NETWORK_JSON.read().unwrap(); + net_json + .nodes + .iter() + .enumerate() + .map(|(i, n) | (i, n.clone_to_transit())) + .collect() + }).await.unwrap(); + + let message = json!( + { + "event": PublishedChannels::NetworkTree.to_string(), + "data": data, + } + ).to_string(); + channels.send(PublishedChannels::NetworkTree, message).await; +} \ No newline at end of file