mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2025-02-25 18:55:32 -06:00
Initial try at a tree view. Includes a few formatting options that will be useful elsewhere.
This commit is contained in:
parent
d03a4f5b19
commit
0f759cbe5c
@ -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}"
|
||||
|
@ -53,3 +53,52 @@ export function lerpGreenToRedViaOrange(value, max) {
|
||||
}
|
||||
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 = "<span class='overlayThroughputWrapper'>";
|
||||
blob += "<span class='overlayThroughputBar'>";
|
||||
for (let i=0; i<100; i+=10) {
|
||||
let color = lerpGreenToRedViaOrange(100-i, 100);
|
||||
if (percent < i) {
|
||||
blob += "░";
|
||||
} else {
|
||||
blob += "<span style='color: " + color + "'>█</span>";
|
||||
}
|
||||
}
|
||||
blob += "</span>";
|
||||
|
||||
blob += "<span class='overlayThroughputNumber' style='color: white; font-weight: bold;'>" + scaleNumber(throughput * 8, 1) + "bps</span>";
|
||||
blob += "</span>";
|
||||
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 = "<span class='overlayThroughputWrapper'>";
|
||||
blob += "<span class='overlayThroughputBar'>";
|
||||
for (let i=0; i<100; i+=10) {
|
||||
let color = lerpGreenToRedViaOrange(100-i, 100);
|
||||
if (percent < i) {
|
||||
blob += "░";
|
||||
} else {
|
||||
blob += "<span style='color: " + color + "'>█</span>";
|
||||
}
|
||||
}
|
||||
blob += "</span>";
|
||||
|
||||
blob += "<span class='overlayThroughputNumber' style='color: white; font-weight: bold;'>" + parseFloat(rtt).toFixed(0) + " ms</span>";
|
||||
blob += "</span>";
|
||||
return blob;
|
||||
}
|
292
src/rust/lqosd/src/node_manager/js_build/src/tree.js
Normal file
292
src/rust/lqosd/src/node_manager/js_build/src/tree.js
Normal file
@ -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<tree.length; i++) {
|
||||
let nodeId = tree[i][0];
|
||||
let node = tree[i][1];
|
||||
|
||||
if (nodeId === parent) {
|
||||
$("#nodeName").text(node.name);
|
||||
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[1] * 1000 * 1000, 0);
|
||||
}
|
||||
$("#parentLimits").text(limit);
|
||||
$("#parentTpD").html(formatThroughput(node.current_throughput[0] * 8, node.max_throughput[0]));
|
||||
$("#parentTpU").html(formatThroughput(node.current_throughput[1] * 8, node.max_throughput[1]));
|
||||
console.log(node);
|
||||
$("#parentRttD").html(formatRtt(node.rtts[0]));
|
||||
$("#parentRttU").html(formatRtt(node.rtts[1]));
|
||||
}
|
||||
|
||||
if (node.immediate_parent !== null && node.immediate_parent === parent) {
|
||||
let row = buildRow(i);
|
||||
tbody.appendChild(row);
|
||||
iterateChildren(i, tbody, 1);
|
||||
}
|
||||
}
|
||||
treeTable.appendChild(tbody);
|
||||
|
||||
// Clear and apply
|
||||
let target = document.getElementById("tree");
|
||||
clearDiv(target)
|
||||
target.appendChild(treeTable);
|
||||
|
||||
subscribeWS(["NetworkTree"], onMessage);
|
||||
});
|
||||
}
|
||||
|
||||
function iterateChildren(idx, tBody, depth) {
|
||||
for (let i=0; i<tree.length; i++) {
|
||||
let node = tree[i][1];
|
||||
if (node.immediate_parent !== null && node.immediate_parent === tree[idx][0]) {
|
||||
let row = buildRow(i, depth);
|
||||
tBody.appendChild(row);
|
||||
iterateChildren(i, tBody, depth+1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildRow(i, depth=0) {
|
||||
let node = tree[i][1];
|
||||
let nodeId = tree[i][0];
|
||||
let row = document.createElement("tr");
|
||||
row.classList.add("small");
|
||||
let col = document.createElement("td");
|
||||
let nodeName = "";
|
||||
if (depth > 0) {
|
||||
nodeName += "└";
|
||||
}
|
||||
for (let j=1; j<depth; j++) {
|
||||
nodeName += "─";
|
||||
}
|
||||
if (depth > 0) nodeName += " ";
|
||||
nodeName += "<a href='/tree.html?parent=" + nodeId + "'>";
|
||||
nodeName += node.name;
|
||||
nodeName += "</a>";
|
||||
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();
|
@ -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))
|
||||
}
|
20
src/rust/lqosd/src/node_manager/local_api/network_tree.rs
Normal file
20
src/rust/lqosd/src/node_manager/local_api/network_tree.rs
Normal file
@ -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<usize>
|
||||
) -> Json<Vec<(usize, NetworkJsonTransport)>> {
|
||||
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)
|
||||
}
|
@ -80,3 +80,21 @@ body.dark-mode {
|
||||
}
|
||||
.dashEditButton { }
|
||||
.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;
|
||||
}
|
@ -49,7 +49,7 @@
|
||||
</li>
|
||||
<!-- Tree -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link">
|
||||
<a class="nav-link" href="tree.html?parent=0">
|
||||
<i class="fa fa-tree nav-icon"></i> Tree
|
||||
</a>
|
||||
</li>
|
||||
|
29
src/rust/lqosd/src/node_manager/static2/tree.html
Normal file
29
src/rust/lqosd/src/node_manager/static2/tree.html
Normal file
@ -0,0 +1,29 @@
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<h5><i class="fa fa-tree"></i> Network Tree (<span id="nodeName"></span>)</h5>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Limits:</td>
|
||||
<td class="small"><span id="parentLimits"></span></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">Throughput:</td>
|
||||
<td><span id="parentTpD"></span></td>
|
||||
<td><span id="parentTpU"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">RTT:</td>
|
||||
<td><span id="parentRttD"></span></td>
|
||||
<td><span id="parentRttU"></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12" id="tree">
|
||||
<p><i class="fa fa-spin fa-spinner"></i> Loading, Please Wait</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="tree.js"></script>
|
@ -29,7 +29,7 @@ pub(super) fn static_routes() -> Result<Router> {
|
||||
// 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
|
||||
|
@ -19,4 +19,5 @@ pub enum PublishedChannels {
|
||||
Ram,
|
||||
TreeSummary,
|
||||
QueueStatsTotal,
|
||||
NetworkTree,
|
||||
}
|
||||
|
@ -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<PubSub>) {
|
||||
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()),
|
||||
);
|
||||
}
|
||||
}
|
31
src/rust/lqosd/src/node_manager/ws/ticker/network_tree.rs
Normal file
31
src/rust/lqosd/src/node_manager/ws/ticker/network_tree.rs
Normal file
@ -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<PubSub>) {
|
||||
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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user