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 @@
-
+
Tree
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: |
+ |
+ |
+
+
+
+
+
+
+
\ 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