This commit is contained in:
Robert Chacon committed 2026-03-16 05:11:44 -06:00
1 parent cee716c0d2
commit 4d06b86d8a
32 files changed
+884 -248

No files matched your search

@@ -128,10 +128,10 @@ function mdToHtml(md){
const cells = rowMatch[1].split('|').map(c => c.trim());
bodyRows.push(cells);
}
let html = '<table class="table table-sm table-striped table-bordered">';
let html = '<div class="table-responsive lqos-table-wrap"><table class="lqos-table lqos-table-compact mb-0">';
html += '<thead><tr>' + headerCells.map(h => `<th>${h}</th>`).join('') + '</tr></thead>';
html += '<tbody>' + bodyRows.map(r => '<tr>' + r.map(c => `<td>${c}</td>`).join('') + '</tr>').join('') + '</tbody>';
html += '</table>';
html += '</table></div>';
out.push(html);
} else {
out.push(l);
@@ -19,6 +19,7 @@ import {CakeQueueLength} from "./graphs/cake_queue_length";
import {CakeTraffic} from "./graphs/cake_traffic";
import {CakeMarks} from "./graphs/cake_marks";
import {CakeDrops} from "./graphs/cake_drops";
import {getNodeIdMap, linkToTreeNode} from "./executive_utils";
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
@@ -63,6 +64,54 @@ function isElementVisible(el) {
return !!(el && el.offsetWidth > 0 && el.offsetHeight > 0);
}
function loadingBlockHtml(label, sizeClass = "") {
const size = sizeClass ? ` ${sizeClass}` : "";
return `<div class="lqos-loading-block${size}"><i class="fa fa-spinner fa-spin"></i><span>${label}</span></div>`;
}
function initTooltipsWithin(rootEl = document) {
if (typeof bootstrap === "undefined" || !bootstrap.Tooltip) {
return;
}
const elements = rootEl.querySelectorAll('[data-bs-toggle="tooltip"]');
elements.forEach((element) => {
if (bootstrap.Tooltip.getOrCreateInstance) {
bootstrap.Tooltip.getOrCreateInstance(element);
} else {
new bootstrap.Tooltip(element);
}
});
}
function applyParentNodeLink(parentNodeName) {
const parentNodeEl = document.getElementById("parentNode");
if (!parentNodeEl) {
return;
}
parentNodeEl.textContent = parentNodeName || "";
if (!parentNodeName) {
parentNodeEl.removeAttribute("href");
parentNodeEl.removeAttribute("title");
parentNodeEl.style.pointerEvents = "none";
return;
}
parentNodeEl.style.pointerEvents = "";
getNodeIdMap().then((nodeIdLookup) => {
const href = linkToTreeNode(parentNodeName, nodeIdLookup);
if (!href) {
parentNodeEl.removeAttribute("href");
parentNodeEl.removeAttribute("title");
parentNodeEl.style.pointerEvents = "none";
return;
}
parentNodeEl.href = href;
parentNodeEl.title = `Open ${parentNodeName} in Tree`;
});
}
function resizeGraphIfVisible(graph) {
if (!graph || !graph.chart || typeof graph.chart.resize !== "function") {
return;
@@ -278,9 +327,11 @@ function renderCakeGraphShell() {
</div>
<div class="col-3">
Queue Memory: <span id="cakeQueueMemory">?</span>
<div class="text-muted small mt-1">Queue Type: <span id="cakeQueueType">?</span></div>
</div>
</div>
`;
setQueueTypeDisplay("");
}
function applyCakeMessage(msg) {
@@ -374,6 +425,36 @@ function ipToString(ip) {
return String(ip);
}
function parseDirectionalSqmToken(token) {
const raw = (token ?? "").toString().trim().toLowerCase();
if (!raw) {
return { down: "", up: "" };
}
if (!raw.includes("/")) {
return { down: raw, up: raw };
}
const [down, up] = raw.split("/", 2);
return {
down: (down ?? "").toString().trim(),
up: (up ?? "").toString().trim(),
};
}
function formatQueueTypeDisplay(sqmToken) {
const { down, up } = parseDirectionalSqmToken(sqmToken);
const downLabel = down || "Unknown";
const upLabel = up || down || "Unknown";
return `${downLabel} / ${upLabel}`;
}
function setQueueTypeDisplay(sqmToken) {
const queueTypeEl = document.getElementById("cakeQueueType");
if (!queueTypeEl) {
return;
}
queueTypeEl.textContent = formatQueueTypeDisplay(sqmToken);
}
function requestCircuitById(onSuccess, onError) {
listenOnce("CircuitByIdResult", (msg) => {
if (!msg || !msg.ok) {
@@ -595,8 +676,11 @@ function updateTrafficTab(msg) {
let target = document.getElementById("allTraffic");
let visibleRowCount = 0;
let tableWrap = document.createElement("div");
tableWrap.classList.add("lqos-table-wrap");
let table = document.createElement("table");
table.classList.add("table", "table-sm", "table-striped");
table.classList.add("lqos-table", "lqos-table-tight");
let thead = document.createElement("thead", "small");
thead.style.fontSize = "0.8em";
@@ -731,7 +815,7 @@ function updateTrafficTab(msg) {
const excludeBtn = document.createElement("button");
excludeBtn.type = "button";
excludeBtn.className = "btn btn-outline-secondary btn-sm";
excludeBtn.textContent = "Exclude RTT…";
excludeBtn.textContent = "Exclude";
excludeBtn.disabled = !remoteIp;
excludeBtn.title = "Open a wizard to exclude RTT samples for this remote IP/CIDR (requires saving in Flow Tracking config).";
excludeBtn.addEventListener("click", (e) => {
@@ -828,8 +912,9 @@ function updateTrafficTab(msg) {
table.appendChild(tbody);
tableWrap.appendChild(table);
clearDiv(target);
target.appendChild(table);
target.appendChild(tableWrap);
$("#trafficFlowCount").text(visibleRowCount);
return visibleRowCount;
}
@@ -902,12 +987,10 @@ function fillLiveDevices(devices) {
const curP95 = device.rtt_current_p95_nanos || {};
const totP50 = device.rtt_total_p50_nanos || {};
const totP95 = device.rtt_total_p95_nanos || {};
rttDown.innerHTML =
"<div class='tiny'>C: " +
formatRttPair(curP50.down, curP95.down) +
"</div><div class='tiny text-secondary'>T: " +
formatRttPair(totP50.down, totP95.down) +
"</div>";
rttDown.innerHTML = formatRttMetricBlock(
formatRttPair(curP50.down, curP95.down),
formatRttPair(totP50.down, totP95.down)
);
}
if (rttUp !== null) {
@@ -915,12 +998,10 @@ function fillLiveDevices(devices) {
const curP95 = device.rtt_current_p95_nanos || {};
const totP50 = device.rtt_total_p50_nanos || {};
const totP95 = device.rtt_total_p95_nanos || {};
rttUp.innerHTML =
"<div class='tiny'>C: " +
formatRttPair(curP50.up, curP95.up) +
"</div><div class='tiny text-secondary'>T: " +
formatRttPair(totP50.up, totP95.up) +
"</div>";
rttUp.innerHTML = formatRttMetricBlock(
formatRttPair(curP50.up, curP95.up),
formatRttPair(totP50.up, totP95.up)
);
}
if (tcp_retransmitsDown !== null) {
@@ -945,6 +1026,19 @@ function fillLiveDevices(devices) {
});
}
function formatRttMetricBlock(currentText, totalText) {
return "<div class='lqos-rtt-metric'>" +
"<div class='lqos-rtt-metric-line'>" +
"<span class='lqos-rtt-metric-label'>C:</span>" +
"<span class='lqos-rtt-metric-value'>" + currentText + "</span>" +
"</div>" +
"<div class='lqos-rtt-metric-line text-secondary'>" +
"<span class='lqos-rtt-metric-label'>T:</span>" +
"<span class='lqos-rtt-metric-value'>" + totalText + "</span>" +
"</div>" +
"</div>";
}
function initialDevices(circuits) {
let target = document.getElementById("devices");
clearDiv(target);
@@ -969,16 +1063,21 @@ function initialDevices(circuits) {
name.innerHTML = "<i class='fa fa-computer'></i> " + circuit.device_name;
d.appendChild(name);
let infoTableWrap = document.createElement("div");
infoTableWrap.classList.add("lqos-table-wrap");
let infoTable = document.createElement("table");
infoTable.classList.add("table", "table-sm", "table-striped");
infoTable.classList.add("lqos-table", "lqos-table-tight");
let tbody = document.createElement("tbody");
// MAC Row
let tr = document.createElement("tr");
let td = document.createElement("td");
td.innerHTML = "<b>MAC Address</b>";
td.textContent = "MAC Address";
td.classList.add("table-label-cell");
tr.appendChild(td);
td = document.createElement("td");
td.classList.add("table-value-cell");
td.classList.add("redactable");
td.colSpan = 2;
td.innerHTML = circuit.mac;
@@ -988,9 +1087,11 @@ function initialDevices(circuits) {
// Comment Row
let tr2 = document.createElement("tr");
td = document.createElement("td");
td.innerHTML = "<b>Comment</b>";
td.textContent = "Comment";
td.classList.add("table-label-cell");
tr2.appendChild(td);
td = document.createElement("td");
td.classList.add("table-value-cell");
td.colSpan = 2;
td.innerHTML = circuit.comment;
tr2.appendChild(td);
@@ -999,12 +1100,14 @@ function initialDevices(circuits) {
// IPv4 Row
let tr3 = document.createElement("tr");
td = document.createElement("td");
td.innerHTML = "<b>IPv4 Address(es)</b>";
td.textContent = "IPv4 Address(es)";
td.classList.add("table-label-cell");
tr3.appendChild(td);
td = document.createElement("td");
td.classList.add("table-value-cell");
td.colSpan = 2;
let ipv4Table = document.createElement("table");
ipv4Table.classList.add("table", "table-sm");
ipv4Table.classList.add("lqos-table", "lqos-table-tight");
let ipv4Body = document.createElement("tbody");
circuit.ipv4.forEach((ip) => {
const ipStr = ipToString(ip[0]);
@@ -1036,13 +1139,15 @@ function initialDevices(circuits) {
// IPv6 Row
let tr4 = document.createElement("tr");
td = document.createElement("td");
td.innerHTML = "<b>IPv6 Address(es)</b>";
td.textContent = "IPv6 Address(es)";
td.classList.add("table-label-cell");
tr4.appendChild(td);
td = document.createElement("td");
td.classList.add("table-value-cell");
td.colSpan = 2;
let ipv6 = document.createElement("table");
ipv6.classList.add("table", "table-sm");
ipv6.classList.add("lqos-table", "lqos-table-tight");
let ipv6Body = document.createElement("tbody");
circuit.ipv6.forEach((ip) => {
const ipStr = ipToString(ip[0]);
@@ -1080,9 +1185,11 @@ function initialDevices(circuits) {
// Placeholder for Last Seen
let tr8 = document.createElement("tr");
td = document.createElement("td");
td.innerHTML = "<b>Last Seen</b>";
td.textContent = "Last Seen";
td.classList.add("table-label-cell");
tr8.appendChild(td);
td = document.createElement("td");
td.classList.add("table-value-cell");
td.colSpan = 2;
td.id = "last_seen_" + circuit.device_id;
td.innerHTML = "<i class='fa fa-spinner fa-spin'></i> Loading...";
@@ -1092,13 +1199,16 @@ function initialDevices(circuits) {
// Placeholder for throughput
let tr5 = document.createElement("tr");
td = document.createElement("td");
td.innerHTML = "<b>Throughput</b>";
td.textContent = "Throughput";
td.classList.add("table-label-cell");
tr5.appendChild(td);
td = document.createElement("td");
td.classList.add("table-value-cell");
td.id = "throughputDown_" + circuit.device_id;
td.innerHTML = "<i class='fa fa-spinner fa-spin'></i> Loading...";
tr5.appendChild(td);
td = document.createElement("td");
td.classList.add("table-value-cell");
td.id = "throughputUp_" + circuit.device_id;
td.innerHTML = "<i class='fa fa-spinner fa-spin'></i> Loading...";
tr5.appendChild(td);
@@ -1107,35 +1217,42 @@ function initialDevices(circuits) {
// Placeholder for RTT
let tr6 = document.createElement("tr");
td = document.createElement("td");
td.innerHTML = "<b>RTT P50/P95</b>";
td.textContent = "RTT P50/P95";
td.classList.add("table-label-cell");
tr6.appendChild(td);
td = document.createElement("td");
td.classList.add("table-value-cell", "lqos-rtt-metric-cell");
td.id = "rttDown_" + circuit.device_id;
td.innerHTML = "<span class='text-secondary'>Sampling...</span>";
td.innerHTML = formatRttMetricBlock("Sampling...", "Sampling...");
tr6.appendChild(td);
td = document.createElement("td");
td.classList.add("table-value-cell", "lqos-rtt-metric-cell");
td.id = "rttUp_" + circuit.device_id;
td.innerHTML = "<span class='text-secondary'>Sampling...</span>";
td.innerHTML = formatRttMetricBlock("Sampling...", "Sampling...");
tr6.appendChild(td);
tbody.appendChild(tr6);
// Placeholder for TCP Retransmits
let tr7 = document.createElement("tr");
td = document.createElement("td");
td.innerHTML = "<b>TCP Re-Xmits</b>";
td.textContent = "TCP Re-Xmits";
td.classList.add("table-label-cell");
tr7.appendChild(td);
td = document.createElement("td");
td.classList.add("table-value-cell");
td.id = "tcp_retransmitsDown_" + circuit.device_id;
td.innerHTML = "<i class='fa fa-spinner fa-spin'></i> Loading...";
tr7.appendChild(td);
td = document.createElement("td");
td.classList.add("table-value-cell");
td.id = "tcp_retransmitsUp_" + circuit.device_id;
td.innerHTML = "<i class='fa fa-spinner fa-spin'></i> Loading...";
tr7.appendChild(td);
tbody.appendChild(tr7);
infoTable.appendChild(tbody);
d.appendChild(infoTable);
infoTableWrap.appendChild(infoTable);
d.appendChild(infoTableWrap);
// Graph container (2x2)
let graphCol = document.createElement("div");
@@ -1152,7 +1269,7 @@ function initialDevices(circuits) {
let div = document.createElement("div");
div.id = divId;
div.style.height = "250px";
div.innerHTML = "<i class='fa fa-spinner fa-spin'></i> Loading...";
div.innerHTML = loadingBlockHtml("Loading chart…", "lqos-loading-block-sm");
col.appendChild(div);
graphRow.appendChild(col);
deviceGraphs[divId] = graphFactory(divId);
@@ -1339,18 +1456,21 @@ function download(dataurl, filename) {
}
function loadInitial() {
initTooltipsWithin(document);
initExcludeRttToggle();
initFlowFilters();
requestCircuitById((circuits) => {
let circuit = circuits[0];
$("#circuitName").text(circuit.circuit_name);
$("#parentNode").text(circuit.parent_node);
$("#circuitName").attr("title", circuit.circuit_name || "");
applyParentNodeLink(circuit.parent_node);
$("#bwMax").text(formatMbps(circuit.download_max_mbps) + " / " + formatMbps(circuit.upload_max_mbps));
$("#bwMin").text(formatMbps(circuit.download_min_mbps) + " / " + formatMbps(circuit.upload_min_mbps));
plan = {
down: toNumber(circuit.download_max_mbps, 0),
up: toNumber(circuit.upload_max_mbps, 0),
};
setQueueTypeDisplay(circuit.sqm_override || "");
initialDevices(circuits);
speedometer = new BitsPerSecondGauge("bitsGauge", "Plan");
qooGauge = new QooScoreGauge("qooGauge");
@@ -279,6 +279,7 @@ export function renderConfigMenu(currentPage) {
{ href: "config_interface.html", icon: "fa-chain", text: "Network Mode", id: "interface" },
{ href: "config_queues.html", icon: "fa-car", text: "Queues", id: "queues" },
{ href: "config_stormguard.html", icon: "fa-bolt", text: "StormGuard", id: "stormguard" },
{ href: "config_treeguard.html", icon: "fa-shield-halved", text: "TreeGuard", id: "treeguard" },
{ href: "config_lts.html", icon: "fa-line-chart", text: "LibreQoS Insight", id: "lts" },
{ href: "config_iprange.html", icon: "fa-address-card", text: "IP Ranges", id: "iprange" },
{ href: "config_flows.html", icon: "fa-arrow-circle-down", text: "Flow Tracking", id: "flows" },
@@ -91,7 +91,8 @@ function loadUsers() {
return;
}
const table = $('<table class="table table-striped">')
const tableWrap = $('<div class="table-responsive lqos-table-wrap">');
const table = $('<table class="lqos-table lqos-table-compact mb-0">')
.append('<thead><tr><th>Username</th><th>Role</th><th>Actions</th></tr></thead>');
const tbody = $('<tbody>');
@@ -112,7 +113,8 @@ function loadUsers() {
});
table.append(tbody);
userList.append(table);
tableWrap.append(table);
userList.append(tableWrap);
// Attach edit handlers
$('.edit-user').on('click', function() {
@@ -78,8 +78,10 @@ function renderSummary(data) {
const right = document.createElement('div');
right.className = 'col-12 col-lg-4';
const tableWrap = document.createElement("div");
tableWrap.classList.add("lqos-table-wrap");
const table = document.createElement("table");
table.classList.add("table", "table-striped", "table-sm");
table.classList.add("lqos-table", "lqos-table-compact");
const thead = document.createElement("thead");
thead.appendChild(theading("CPU"));
thead.appendChild(theading("DL Circuits"));
@@ -128,7 +130,8 @@ function renderSummary(data) {
});
table.appendChild(tbody);
left.appendChild(table);
tableWrap.appendChild(table);
left.appendChild(tableWrap);
// Right column pie chart (DL Max per CPU)
const chartDiv = document.createElement('div');
@@ -205,8 +208,10 @@ function renderCircuits(page) {
smallNote.innerText = `Total: ${page.total.toLocaleString()} circuits`;
target.appendChild(smallNote);
const tableWrap = document.createElement("div");
tableWrap.classList.add("lqos-table-wrap");
const table = document.createElement("table");
table.classList.add("table", "table-striped", "table-sm");
table.classList.add("lqos-table", "lqos-table-compact");
const thead = document.createElement("thead");
thead.appendChild(theading("Circuit ID"));
thead.appendChild(theading("Circuit Name"));
@@ -249,7 +254,8 @@ function renderCircuits(page) {
});
table.appendChild(tbody);
target.appendChild(table);
tableWrap.appendChild(table);
target.appendChild(tableWrap);
}
function fetchSummary() {
@@ -56,7 +56,7 @@ export class CircuitCapacityDash extends BaseDashlet {
const cached = this.timeCache.get();
let table = document.createElement("table");
table.classList.add("dash-table", "table-sm", "small");
table.classList.add("dash-table", "lqos-table", "lqos-table-compact", "small");
let thead = document.createElement("thead");
thead.classList.add("small");
thead.appendChild(theading("Circuit"));
@@ -101,4 +101,4 @@ export class CircuitCapacityDash extends BaseDashlet {
target.appendChild(table);
}
}
}
}
@@ -26,8 +26,7 @@ export class Top10EndpointsByCountry extends DashletBaseInsight {
buildContainer() {
let base = super.buildContainer();
base.style.height = "250px";
base.style.overflow = "auto";
base.classList.add("dashbox-body-scroll", "dashbox-body-scroll-top10");
return base;
}
@@ -100,4 +99,4 @@ export class Top10EndpointsByCountry extends DashletBaseInsight {
target.appendChild(t);
}
}
}
}
@@ -39,7 +39,7 @@ export class EtherProtocols extends DashletBaseInsight {
let target = document.getElementById(this.id);
let t = document.createElement("table");
t.classList.add("dash-table", "table-sm", "small");
t.classList.add("dash-table", "lqos-table", "lqos-table-compact", "small");
let th = document.createElement("thead");
th.classList.add("small");
@@ -81,4 +81,4 @@ export class EtherProtocols extends DashletBaseInsight {
target.appendChild(t);
}
}
}
}
@@ -39,7 +39,7 @@ export class IpProtocols extends DashletBaseInsight {
let target = document.getElementById(this.id);
let t = document.createElement("table");
t.classList.add("dash-table", "table-sm", "small");
t.classList.add("dash-table", "lqos-table", "lqos-table-compact", "small");
let th = document.createElement("thead");
th.classList.add("small");
@@ -69,4 +69,4 @@ export class IpProtocols extends DashletBaseInsight {
target.appendChild(t);
}
}
}
}
@@ -57,7 +57,7 @@ export class LtsShaperStatus extends BaseDashlet {
let target = document.getElementById(this.contentId);
let table = document.createElement("table");
table.classList.add("table", "table-sm", "small");
table.classList.add("lqos-table", "lqos-table-compact", "small");
let thead = document.createElement("thead");
thead.appendChild(theading(""));
thead.appendChild(theading("Shaper"));
@@ -81,7 +81,10 @@ export class LtsShaperStatus extends BaseDashlet {
})
table.appendChild(tbody);
clearDiv(target);
target.appendChild(table);
const tableWrap = document.createElement("div");
tableWrap.classList.add("lqos-table-wrap");
tableWrap.appendChild(table);
target.appendChild(tableWrap);
//console.log(data);
});
@@ -36,8 +36,7 @@ export class Top10Downloaders extends DashletBaseInsight {
buildContainer() {
let base = super.buildContainer();
base.style.height = "250px";
base.style.overflow = "auto";
base.classList.add("dashbox-body-scroll", "dashbox-body-scroll-top10");
return base;
}
@@ -82,7 +81,7 @@ export class Top10Downloaders extends DashletBaseInsight {
let target = document.getElementById(this.id);
let table = document.createElement("table");
table.classList.add("table", "table-sm", "small");
table.classList.add("lqos-table", "lqos-table-compact", "small");
let thead = document.createElement("thead");
thead.appendChild(theading("Circuit"));
thead.appendChild(theading("Bytes Downloaded"));
@@ -111,7 +110,10 @@ export class Top10Downloaders extends DashletBaseInsight {
})
table.appendChild(tbody);
clearDashDiv(this.id, target);
target.appendChild(table);
const tableWrap = document.createElement("div");
tableWrap.classList.add("lqos-table-wrap");
tableWrap.appendChild(table);
target.appendChild(tableWrap);
});
wsClient.send({ LtsTop10Downloaders: { seconds } });
}
@@ -42,8 +42,7 @@ export class Top10FlowsBytes extends DashletBaseInsight {
buildContainer() {
let base = super.buildContainer();
base.style.height = "250px";
base.style.overflow = "auto";
base.classList.add("dashbox-body-scroll", "dashbox-body-scroll-top10");
return base;
}
@@ -57,7 +56,7 @@ export class Top10FlowsBytes extends DashletBaseInsight {
let target = document.getElementById(this.id);
let t = document.createElement("table");
t.classList.add("dash-table", "table-sm", "small");
t.classList.add("dash-table", "lqos-table", "lqos-table-compact", "small");
let th = document.createElement("thead");
th.classList.add("small");
@@ -68,7 +67,9 @@ export class Top10FlowsBytes extends DashletBaseInsight {
th.appendChild(theading("Total"));
th.appendChild(theading("RTT", 2));
th.appendChild(theading("TCP Retransmits", 2));
th.appendChild(theading("Remote ASN"));
const asnHeading = theading("Remote ASN");
asnHeading.classList.add("lqos-asn-cell");
th.appendChild(asnHeading);
t.appendChild(th);
let tbody = document.createElement("tbody");
@@ -138,13 +139,16 @@ export class Top10FlowsBytes extends DashletBaseInsight {
row.appendChild(tcp2);
let asn = document.createElement("td");
asn.innerText = r.remote_asn_name;
if (asn.innerText === "") {
asn.innerText = r.remote_ip;
}
if (asn.innerText.length > 13) {
asn.classList.add("tiny");
asn.classList.add("lqos-asn-cell");
const asnLabel = (r.remote_asn_name && r.remote_asn_name.length > 0) ? r.remote_asn_name : r.remote_ip;
const asnText = document.createElement("span");
asnText.classList.add("lqos-table-cell-ellipsis");
if (asnLabel && asnLabel.length > 13) {
asnText.classList.add("tiny");
}
asnText.textContent = asnLabel || "";
asnText.title = asnLabel || "";
asn.appendChild(asnText);
row.appendChild(asn);
t.appendChild(row);
@@ -170,7 +174,7 @@ export class Top10FlowsBytes extends DashletBaseInsight {
let target = document.getElementById(this.id);
let table = document.createElement("table");
table.classList.add("table", "table-sm", "small");
table.classList.add("lqos-table", "lqos-table-compact", "small");
let thead = document.createElement("thead");
thead.appendChild(theading("Circuit"));
thead.appendChild(theading("Protocol"));
@@ -180,7 +184,9 @@ export class Top10FlowsBytes extends DashletBaseInsight {
thead.appendChild(theading("RTT UL"));
thead.appendChild(theading("Rxmits DL"));
thead.appendChild(theading("Rxmits UL"));
thead.appendChild(theading("ASN"));
const asnHeading = theading("ASN");
asnHeading.classList.add("lqos-asn-cell");
thead.appendChild(asnHeading);
table.appendChild(thead);
let tbody = document.createElement("tbody");
@@ -211,16 +217,26 @@ export class Top10FlowsBytes extends DashletBaseInsight {
} else {
tr.appendChild(simpleRowHtml(formatRetransmit(row.rxmit_up)));
}
if (row.asn_name === null) {
row.asn_name = "-";
} else {
tr.appendChild(simpleRow(row.asn_name));
const asnCell = document.createElement("td");
asnCell.classList.add("lqos-asn-cell");
const asnText = document.createElement("span");
const asnLabel = row.asn_name === null ? "-" : row.asn_name;
asnText.classList.add("lqos-table-cell-ellipsis");
if (asnLabel && asnLabel.length > 13) {
asnText.classList.add("tiny");
}
asnText.textContent = asnLabel || "";
asnText.title = asnLabel || "";
asnCell.appendChild(asnText);
tr.appendChild(asnCell);
tbody.appendChild(tr);
})
table.appendChild(tbody);
clearDashDiv(this.id, target);
target.appendChild(table);
const tableWrap = document.createElement("div");
tableWrap.classList.add("lqos-table-wrap");
tableWrap.appendChild(table);
target.appendChild(tableWrap);
});
wsClient.send({ LtsTopFlows: { seconds } });
}
@@ -31,8 +31,7 @@ export class Top10FlowsRate extends DashletBaseInsight {
buildContainer() {
let base = super.buildContainer();
base.style.height = "250px";
base.style.overflow = "auto";
base.classList.add("dashbox-body-scroll", "dashbox-body-scroll-top10");
return base;
}
@@ -56,7 +55,9 @@ export class Top10FlowsRate extends DashletBaseInsight {
th.appendChild(theading("Total"));
th.appendChild(theading("RTT", 2));
th.appendChild(theading("TCP Retransmits", 2));
th.appendChild(theading("Remote ASN"));
const asnHeading = theading("Remote ASN");
asnHeading.classList.add("lqos-asn-cell");
th.appendChild(asnHeading);
t.appendChild(th);
let tbody = document.createElement("tbody");
@@ -126,13 +127,16 @@ export class Top10FlowsRate extends DashletBaseInsight {
row.appendChild(tcp2);
let asn = document.createElement("td");
asn.innerText = r.remote_asn_name;
if (asn.innerText === "") {
asn.innerText = r.remote_ip;
}
if (asn.innerText.length > 13) {
asn.classList.add("tiny");
asn.classList.add("lqos-asn-cell");
const asnLabel = (r.remote_asn_name && r.remote_asn_name.length > 0) ? r.remote_asn_name : r.remote_ip;
const asnText = document.createElement("span");
asnText.classList.add("lqos-table-cell-ellipsis");
if (asnLabel && asnLabel.length > 13) {
asnText.classList.add("tiny");
}
asnText.textContent = asnLabel || "";
asnText.title = asnLabel || "";
asn.appendChild(asnText);
row.appendChild(asn);
t.appendChild(row);
@@ -35,7 +35,7 @@ export class TopTreeSummary extends DashletBaseInsight {
let t = document.createElement("table");
t.id = this.id + "_table";
t.classList.add("dash-table", "table-sm", "mytable", "small");
t.classList.add("dash-table", "lqos-table", "lqos-table-compact", "mytable", "small");
let th = document.createElement("thead");
th.classList.add('small');
@@ -40,7 +40,7 @@ export class TreeCapacityDash extends DashletBaseInsight {
let target = document.getElementById(this.id);
let table = document.createElement("table");
table.classList.add("dash-table", "table-sm", "small");
table.classList.add("dash-table", "lqos-table", "lqos-table-compact", "small");
let thead = document.createElement("thead");
thead.classList.add("small");
thead.appendChild(theading("Node"));
@@ -227,8 +227,11 @@ export class TreeGuardActivityDashlet extends BaseDashlet {
const wrap = document.createElement("div");
wrap.classList.add("p-2");
const tableWrap = document.createElement("div");
tableWrap.classList.add("lqos-table-wrap");
const table = document.createElement("table");
table.classList.add("table", "table-sm", "table-striped", "mb-0", "small");
table.classList.add("lqos-table", "lqos-table-compact", "mb-0", "small");
const thead = document.createElement("thead");
thead.classList.add("small");
@@ -243,7 +246,8 @@ export class TreeGuardActivityDashlet extends BaseDashlet {
this.tbody = document.createElement("tbody");
table.appendChild(thead);
table.appendChild(this.tbody);
wrap.appendChild(table);
tableWrap.appendChild(table);
wrap.appendChild(tableWrap);
base.appendChild(wrap);
return base;
}
@@ -243,17 +243,21 @@ export class TreeGuardStatusDashlet extends BaseDashlet {
const wrap = document.createElement("div");
wrap.classList.add("p-2");
const tableWrap = document.createElement("div");
tableWrap.classList.add("lqos-table-wrap");
const table = document.createElement("table");
table.classList.add("table", "table-sm", "mb-2");
table.classList.add("lqos-table", "lqos-table-compact", "mb-0");
const tbody = document.createElement("tbody");
const mkRow = (label, valueEl) => {
const tr = document.createElement("tr");
const tdL = document.createElement("td");
tdL.classList.add("text-muted");
tdL.classList.add("table-label-cell");
tdL.style.width = "45%";
tdL.textContent = label;
const tdV = document.createElement("td");
tdV.classList.add("table-value-cell");
tdV.appendChild(valueEl);
tr.appendChild(tdL);
tr.appendChild(tdV);
@@ -280,10 +284,11 @@ export class TreeGuardStatusDashlet extends BaseDashlet {
tbody.appendChild(mkRow("Last Action", this.lastActionEl));
table.appendChild(tbody);
wrap.appendChild(table);
tableWrap.appendChild(table);
wrap.appendChild(tableWrap);
const warningsHeader = document.createElement("div");
warningsHeader.classList.add("text-muted", "small", "mb-1");
warningsHeader.classList.add("text-muted", "small", "mt-3", "mb-1");
warningsHeader.textContent = "Warnings";
wrap.appendChild(warningsHeader);
@@ -40,8 +40,7 @@ export class Worst10Downloaders extends DashletBaseInsight {
buildContainer() {
let base = super.buildContainer();
base.style.height = "250px";
base.style.overflow = "auto";
base.classList.add("dashbox-body-scroll", "dashbox-body-scroll-top10");
return base;
}
@@ -82,7 +81,7 @@ export class Worst10Downloaders extends DashletBaseInsight {
let target = document.getElementById(this.id);
let table = document.createElement("table");
table.classList.add("table", "table-sm", "small");
table.classList.add("lqos-table", "lqos-table-compact", "small");
let thead = document.createElement("thead");
thead.appendChild(theading("Circuit"));
thead.appendChild(theading("Bytes Downloaded"));
@@ -111,7 +110,10 @@ export class Worst10Downloaders extends DashletBaseInsight {
})
table.appendChild(tbody);
clearDashDiv(this.id, target);
target.appendChild(table);
const tableWrap = document.createElement("div");
tableWrap.classList.add("lqos-table-wrap");
tableWrap.appendChild(table);
target.appendChild(tableWrap);
});
wsClient.send({ LtsWorst10Rtt: { seconds } });
}
@@ -40,8 +40,7 @@ export class Worst10Retransmits extends DashletBaseInsight {
buildContainer() {
let base = super.buildContainer();
base.style.height = "250px";
base.style.overflow = "auto";
base.classList.add("dashbox-body-scroll", "dashbox-body-scroll-top10");
return base;
}
@@ -82,7 +81,7 @@ export class Worst10Retransmits extends DashletBaseInsight {
let target = document.getElementById(this.id);
let table = document.createElement("table");
table.classList.add("table", "table-sm", "small");
table.classList.add("lqos-table", "lqos-table-compact", "small");
let thead = document.createElement("thead");
thead.appendChild(theading("Circuit"));
thead.appendChild(theading("Bytes Downloaded"));
@@ -111,7 +110,10 @@ export class Worst10Retransmits extends DashletBaseInsight {
})
table.appendChild(tbody);
clearDashDiv(this.id, target);
target.appendChild(table);
const tableWrap = document.createElement("div");
tableWrap.classList.add("lqos-table-wrap");
tableWrap.appendChild(table);
target.appendChild(tableWrap);
});
wsClient.send({ LtsWorst10Rxmit: { seconds } });
}
@@ -20,7 +20,7 @@ export function listenExecutiveHeatmaps(onData) {
let siteIdMap = null;
let siteIdMapPromise = null;
export function getSiteIdMap() {
export function getNodeIdMap() {
if (siteIdMap) return Promise.resolve(siteIdMap);
if (siteIdMapPromise) return siteIdMapPromise;
siteIdMapPromise = new Promise((resolve) => {
@@ -42,6 +42,7 @@ export function getSiteIdMap() {
map.set(node.name, id);
}
});
resolveOnce(map);
});
wsClient.send({ NetworkTree: {} });
setTimeout(() => {
@@ -51,6 +52,10 @@ export function getSiteIdMap() {
return siteIdMapPromise;
}
export function getSiteIdMap() {
return getNodeIdMap();
}
export function linkToCircuit(circuitId) {
if (!circuitId) return null;
return `circuit.html?id=${encodeURIComponent(circuitId)}`;
@@ -63,6 +68,13 @@ export function linkToSite(siteName, siteIdLookup) {
return `tree.html?parent=${encodeURIComponent(siteId)}`;
}
export function linkToTreeNode(nodeName, nodeIdLookup) {
if (!nodeName || !nodeIdLookup) return null;
const nodeId = nodeIdLookup.get(nodeName);
if (nodeId === undefined || nodeId === null) return null;
return `tree.html?parent=${encodeURIComponent(nodeId)}`;
}
export function renderCircuitLink(name, circuitId) {
const link = linkToCircuit(circuitId);
if (!link) return `<span class="redactable">${name}</span>`;
@@ -117,8 +129,8 @@ export function renderTable(targetId, columns, rows, emptyMessage) {
return `<tr>${cells}</tr>`;
}).join("");
target.innerHTML = `
<div class="table-responsive">
<table class="table table-sm table-striped align-middle mb-0">
<div class="table-responsive lqos-table-wrap">
<table class="lqos-table lqos-table-compact align-middle mb-0">
${thead}
<tbody>${tbody}</tbody>
</table>
@@ -177,7 +177,7 @@ export function topNTableRow(r) {
export function TopNTableFromMsgData(circuits) {
let t = document.createElement("table");
t.classList.add("table-sm", "dash-table");
t.classList.add("dash-table", "lqos-table", "lqos-table-compact");
t.appendChild(topNTableHeader());
@@ -187,4 +187,4 @@ export function TopNTableFromMsgData(circuits) {
});
t.appendChild(tbody);
return t;
}
}
@@ -292,7 +292,7 @@ function buildDeviceCard(device) {
// Metrics table
const table = document.createElement("table");
table.classList.add("table", "table-sm", "mb-0", "mt-2");
table.classList.add("lqos-table", "lqos-table-tight", "mb-0", "mt-2");
const thead = document.createElement("thead");
const headRow = document.createElement("tr");
@@ -247,6 +247,7 @@ class SiteMapPage {
this.unmappedOpen = false;
this.heavyLayersInstalled = false;
this.heavyLayersInstallQueued = false;
this.heavyLayersFallbackTimer = null;
this.canvas = document.getElementById("siteMapCanvas");
this.statusChip = document.getElementById("siteMapStatusChip");
@@ -792,14 +793,26 @@ class SiteMapPage {
}
this.heavyLayersInstallQueued = true;
const install = () => {
if (this.heavyLayersFallbackTimer !== null) {
window.clearTimeout(this.heavyLayersFallbackTimer);
this.heavyLayersFallbackTimer = null;
}
this.heavyLayersInstallQueued = false;
this.installHeavyGeographyLayers();
};
if (typeof window.requestIdleCallback === "function") {
window.requestIdleCallback(install, { timeout: 1200 });
return;
}
window.setTimeout(install, 0);
const installWhenReady = () => {
if (this.heavyLayersInstalled) {
return;
}
if (this.map?.isStyleLoaded()) {
install();
return;
}
this.map?.once("idle", install);
};
this.map?.once("idle", installWhenReady);
this.heavyLayersFallbackTimer = window.setTimeout(installWhenReady, 1200);
}
installInteractions() {
@@ -228,7 +228,7 @@ function renderSearchResults(data) {
}
searchResults.style.visibility = "visible";
let list = document.createElement("table");
list.classList.add("table", "table-striped");
list.classList.add("lqos-table", "lqos-table-compact", "mb-0");
let tbody = document.createElement("tbody");
data.forEach((item) => {
let r = document.createElement("tr");
@@ -249,7 +249,10 @@ function renderSearchResults(data) {
});
clearDiv(searchResults);
list.appendChild(tbody);
searchResults.appendChild(list);
const wrap = document.createElement("div");
wrap.classList.add("table-responsive", "lqos-table-wrap");
wrap.appendChild(list);
searchResults.appendChild(wrap);
}
function doSearch(search) {
@@ -461,7 +464,7 @@ function initUrgentIssues() {
return;
}
const table = document.createElement('table');
table.className = 'table table-sm table-striped';
table.className = 'lqos-table lqos-table-compact mb-0';
const tbody = document.createElement('tbody');
items.forEach((it) => {
const tr = document.createElement('tr');
@@ -484,7 +487,10 @@ function initUrgentIssues() {
});
table.appendChild(tbody);
holder.innerHTML = '';
holder.appendChild(table);
const tableWrap = document.createElement('div');
tableWrap.className = 'table-responsive lqos-table-wrap';
tableWrap.appendChild(table);
holder.appendChild(tableWrap);
$(holder).off('click').on('click', '.urgent-clear', function (e) {
e.preventDefault();
const id = $(this).data('id');
@@ -29,6 +29,55 @@ const listenOnce = (eventName, handler) => {
wsClient.on(eventName, wrapped);
};
function formatDeviceIp(ip) {
if (typeof ip === "string") {
return ip;
}
if (ip === null || ip === undefined) {
return "-";
}
if (ip instanceof Uint8Array) {
return formatIpBytes(ip);
}
if (Array.isArray(ip)) {
if (ip.every((entry) => Number.isFinite(Number(entry)))) {
return formatIpBytes(ip);
}
return ip.map((entry) => formatDeviceIp(entry)).filter(Boolean).join(", ");
}
if (typeof ip === "object") {
if (ip.V4 !== undefined) {
return formatDeviceIp(ip.V4);
}
if (ip.V6 !== undefined) {
return formatDeviceIp(ip.V6);
}
if (ip.addr !== undefined) {
return formatDeviceIp(ip.addr);
}
if (Array.isArray(ip.data) || ip.data instanceof Uint8Array) {
return formatDeviceIp(ip.data);
}
}
return String(ip);
}
function formatIpBytes(bytes) {
const list = Array.from(bytes);
if (list.length === 4) {
return list.join(".");
}
if (list.length === 16) {
const parts = [];
for (let i = 0; i < list.length; i += 2) {
const part = ((Number(list[i]) || 0) << 8) | (Number(list[i + 1]) || 0);
parts.push(part.toString(16).padStart(4, "0"));
}
return parts.join(":");
}
return list.join(".");
}
function initTooltipsWithin(rootEl) {
if (!rootEl) return;
if (typeof bootstrap === "undefined" || !bootstrap.Tooltip) return;
@@ -86,8 +135,10 @@ function toggleNode(nodeId) {
}
function renderTree() {
const tableWrap = document.createElement("div");
tableWrap.classList.add("lqos-table-wrap");
let treeTable = document.createElement("table");
treeTable.classList.add("table", "table-striped", "table-bordered");
treeTable.classList.add("lqos-table", "lqos-table-tight");
let thead = document.createElement("thead");
thead.appendChild(theading("Name"));
thead.appendChild(theading("Limit"));
@@ -130,7 +181,8 @@ function renderTree() {
// Clear and apply
let target = document.getElementById("tree");
clearDiv(target)
target.appendChild(treeTable);
tableWrap.appendChild(treeTable);
target.appendChild(tableWrap);
initTooltipsWithin(treeTable);
}
@@ -495,55 +547,135 @@ function clientsUpdate(msg) {
let target = document.getElementById("clients");
let table = document.createElement("table");
table.classList.add("table", "table-striped", "table-bordered");
table.classList.add("lqos-table", "lqos-table-tight");
table.appendChild(clientTableHeader());
let tbody = document.createElement("tbody");
clearDiv(target);
const circuits = new Map();
msg.data.forEach((device) => {
if (device.parent_node === myName) {
if (device.parent_node !== myName) {
return;
}
const circuitId = device.circuit_id || `${device.parent_node || ""}:${device.circuit_name || ""}`;
if (!circuits.has(circuitId)) {
circuits.set(circuitId, {
circuit_id: circuitId,
circuit_name: device.circuit_name || "(Unknown circuit)",
parent_node: device.parent_node || "",
plan: {
down: toNumber(device.plan?.down, 0),
up: toNumber(device.plan?.up, 0),
},
device_names: new Set(),
ips: new Set(),
last_seen_nanos: toNumber(device.last_seen_nanos, 0),
bytes_per_second: {down: 0, up: 0},
median_latency: {down: null, up: null},
tcp_packets: {down: 0, up: 0},
tcp_retransmits: {down: 0, up: 0},
});
}
const circuit = circuits.get(circuitId);
if (device.device_name) {
circuit.device_names.add(device.device_name);
}
const ipText = formatDeviceIp(device.ip);
if (ipText && ipText !== "-") {
circuit.ips.add(ipText);
}
circuit.last_seen_nanos = Math.min(circuit.last_seen_nanos, toNumber(device.last_seen_nanos, 0));
circuit.bytes_per_second.down += toNumber(device.bytes_per_second?.down, 0);
circuit.bytes_per_second.up += toNumber(device.bytes_per_second?.up, 0);
circuit.tcp_packets.down += toNumber(device.tcp_packets?.down, 0);
circuit.tcp_packets.up += toNumber(device.tcp_packets?.up, 0);
circuit.tcp_retransmits.down += toNumber(device.tcp_retransmits?.down, 0);
circuit.tcp_retransmits.up += toNumber(device.tcp_retransmits?.up, 0);
const downLatency = toNumber(device.median_latency?.down, 0);
if (downLatency > 0 && (circuit.median_latency.down === null || downLatency > circuit.median_latency.down)) {
circuit.median_latency.down = downLatency;
}
const upLatency = toNumber(device.median_latency?.up, 0);
if (upLatency > 0 && (circuit.median_latency.up === null || upLatency > circuit.median_latency.up)) {
circuit.median_latency.up = upLatency;
}
});
Array.from(circuits.values())
.sort((a, b) => a.circuit_name.localeCompare(b.circuit_name))
.forEach((circuit) => {
let tr = document.createElement("tr");
tr.classList.add("small");
let linkTd = document.createElement("td");
let circuitLink = document.createElement("a");
circuitLink.href = "/circuit.html?id=" + device.circuit_id;
circuitLink.innerText = device.circuit_name;
circuitLink.href = "/circuit.html?id=" + circuit.circuit_id;
circuitLink.innerText = circuit.circuit_name;
circuitLink.classList.add("redactable");
linkTd.appendChild(circuitLink);
tr.appendChild(linkTd);
tr.appendChild(simpleRow(device.device_name, true));
tr.appendChild(simpleRow(device.plan.down + " / " + device.plan.up));
tr.appendChild(simpleRow(device.parent_node, true));
tr.appendChild(simpleRow(device.ip, true));
tr.appendChild(simpleRow(formatLastSeen(device.last_seen_nanos)));
tr.appendChild(simpleRowHtml(formatThroughput(toNumber(device.bytes_per_second.down, 0) * 8, device.plan.down)));
tr.appendChild(simpleRowHtml(formatThroughput(toNumber(device.bytes_per_second.up, 0) * 8, device.plan.up)));
if (device.median_latency !== null) {
tr.appendChild(simpleRowHtml(formatRtt(device.median_latency.down)));
tr.appendChild(simpleRowHtml(formatRtt(device.median_latency.up)));
const deviceNames = Array.from(circuit.device_names);
const deviceCell = simpleRow(
deviceNames.length > 2 ? `${deviceNames[0]}, ${deviceNames[1]} +${deviceNames.length - 2}` : deviceNames.join(", "),
true
);
if (deviceNames.length > 0) {
deviceCell.title = deviceNames.join(", ");
}
tr.appendChild(deviceCell);
tr.appendChild(simpleRow(circuit.plan.down + " / " + circuit.plan.up));
tr.appendChild(simpleRow(circuit.parent_node, true));
const ipList = Array.from(circuit.ips);
const ipCell = simpleRow(
ipList.length > 2 ? `${ipList[0]}, ${ipList[1]} +${ipList.length - 2}` : ipList.join(", "),
true
);
if (ipList.length > 0) {
ipCell.title = ipList.join(", ");
}
tr.appendChild(ipCell);
tr.appendChild(simpleRow(formatLastSeen(circuit.last_seen_nanos)));
tr.appendChild(simpleRowHtml(formatThroughput(circuit.bytes_per_second.down * 8, circuit.plan.down)));
tr.appendChild(simpleRowHtml(formatThroughput(circuit.bytes_per_second.up * 8, circuit.plan.up)));
if (circuit.median_latency.down !== null) {
tr.appendChild(simpleRowHtml(formatRtt(circuit.median_latency.down)));
} else {
tr.appendChild(simpleRow("-"));
}
if (circuit.median_latency.up !== null) {
tr.appendChild(simpleRowHtml(formatRtt(circuit.median_latency.up)));
} else {
tr.appendChild(simpleRow("-"));
}
let retr = 0;
const devicePacketsDown = toNumber(device.tcp_packets.down, 0);
if (devicePacketsDown > 0) {
retr = toNumber(device.tcp_retransmits.down, 0) / devicePacketsDown;
}
tr.appendChild(simpleRowHtml(formatRetransmit(retr)));
retr = 0;
const devicePacketsUp = toNumber(device.tcp_packets.up, 0);
if (devicePacketsUp > 0) {
retr = toNumber(device.tcp_retransmits.up, 0) / devicePacketsUp;
if (circuit.tcp_packets.down > 0) {
retr = circuit.tcp_retransmits.down / circuit.tcp_packets.down;
}
tr.appendChild(simpleRowHtml(formatRetransmit(retr)));
retr = 0;
if (circuit.tcp_packets.up > 0) {
retr = circuit.tcp_retransmits.up / circuit.tcp_packets.up;
}
tr.appendChild(simpleRowHtml(formatRetransmit(retr)));
// Add it
tbody.appendChild(tr);
}
});
});
table.appendChild(tbody);
target.appendChild(table);
const tableWrap = document.createElement("div");
tableWrap.classList.add("lqos-table-wrap");
tableWrap.appendChild(table);
target.appendChild(tableWrap);
}
function onMessage(msg) {
@@ -46,7 +46,7 @@ function loadUnknownIps() {
}
let table = document.createElement("table");
table.classList.add("table", "table-striped");
table.classList.add("lqos-table", "lqos-table-compact");
let thead = document.createElement("thead");
thead.appendChild(theading("IP Address"));
thead.appendChild(theading("Last Seen"));
@@ -64,7 +64,10 @@ function loadUnknownIps() {
});
table.appendChild(tbody);
target.appendChild(table);
const tableWrap = document.createElement("div");
tableWrap.classList.add("lqos-table-wrap");
tableWrap.appendChild(table);
target.appendChild(tableWrap);
});
wsClient.send({ UnknownIps: {} });
}
@@ -1,55 +1,80 @@
<div class="row">
<div class="col-3">
<h5><i class="fa fa-user"></i> <span id="circuitName" class="redactable"></span></h5>
<table class="table">
<tr>
<td style="font-weight: bold">Parent Node</td>
<td><span id="parentNode" class="redactable"></span></td>
</tr>
<tr>
<td style="font-weight: bold">Maximum Speed</td>
<td><span id="bwMax"></span></td>
</tr>
<tr>
<td style="font-weight: bold">Minimum Speed</td>
<td><span id="bwMin"></span></td>
</tr>
<tr>
<td style="font-weight: bold">RTT Metrics</td>
<td>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="excludeRttToggle">
<label class="form-check-label" for="excludeRttToggle">Exclude from RTT</label>
</div>
<div class="form-text">Exclude this circuit from RTT heatmaps and QoO RTT scoring (QoO will show as unknown).</div>
</td>
</tr>
<tr>
<td colspan="2" id="captureButton">
</td>
</tr>
</table>
</div>
<div class="col-3">
<div class="row g-0">
<div class="col-8">
<div id="bitsGauge" style="width: 100%; height: 200px;"></div>
<div class="row lqos-circuit-top-row align-items-stretch">
<div class="col-12 col-lg-3">
<div class="lqos-circuit-panel">
<h5><i class="fa fa-user"></i> <span id="circuitName" class="redactable lqos-ellipsis-inline"></span></h5>
<div class="lqos-table-wrap">
<table class="lqos-table lqos-table-compact">
<tr>
<td class="table-label-cell">Parent Node</td>
<td class="table-value-cell"><a id="parentNode" class="redactable" href="#"></a></td>
</tr>
<tr>
<td class="table-label-cell">Maximum Speed</td>
<td class="table-value-cell"><span id="bwMax"></span></td>
</tr>
<tr>
<td class="table-label-cell">Minimum Speed</td>
<td class="table-value-cell"><span id="bwMin"></span></td>
</tr>
<tr>
<td class="table-label-cell">RTT Metrics</td>
<td class="table-value-cell">
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="excludeRttToggle">
<label class="form-check-label" for="excludeRttToggle">
Exclude
<span
class="ms-1 text-muted"
role="button"
tabindex="0"
aria-label="Exclude from RTT information"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Exclude this circuit from RTT heatmaps and QoO RTT scoring. QoO will show as unknown."
><i class="fas fa-info-circle"></i></span>
</label>
</div>
</td>
</tr>
<tr>
<td colspan="2" id="captureButton">
</td>
</tr>
</table>
</div>
<div class="col-4">
</div>
</div>
<div class="col-12 col-lg-3">
<div class="lqos-circuit-panel lqos-circuit-gauge-panel">
<div class="lqos-circuit-gauge-grid">
<div id="bitsGauge" style="width: 100%; height: 200px;"></div>
<div id="qooGauge" style="width: 100%; height: 200px;"></div>
</div>
</div>
</div>
<div class="col-3" id="throughputGraph">
<i class="fa fa-spinner fa-spin"></i> Loading...
<div class="col-12 col-lg-3">
<div class="lqos-circuit-panel lqos-circuit-chart-panel">
<div id="throughputGraph">
<div class="lqos-loading-block lqos-loading-block-sm">
<i class="fa fa-spinner fa-spin"></i>
<span>Loading throughput…</span>
</div>
</div>
</div>
</div>
<div class="col-3" id="rxmitGraph">
<i class="fa fa-spinner fa-spin"></i> Loading...
<div class="col-12 col-lg-3">
<div class="lqos-circuit-panel lqos-circuit-chart-panel">
<div id="rxmitGraph">
<div class="lqos-loading-block lqos-loading-block-sm">
<i class="fa fa-spinner fa-spin"></i>
<span>Loading retransmits…</span>
</div>
</div>
</div>
</div>
</div>
<ul class="nav nav-tabs" id="myTab" role="tablist">
<ul class="nav nav-tabs lqos-pill-tabs mb-3" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="devs-tab" data-bs-toggle="tab" data-bs-target="#devs" type="button" role="tab" aria-controls="devs" aria-selected="true">
<i class="fa fa-server"></i> Devices
@@ -79,7 +104,12 @@
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="devs" role="tabpanel" aria-labelledby="devs-tab">
<div class="row" id="devices">
<i class="fa fa-spinner fa-spin"></i> Loading Devices...
<div class="col-12">
<div class="lqos-loading-block lqos-loading-block-lg">
<i class="fa fa-spinner fa-spin"></i>
<span>Loading devices…</span>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="sankey" role="tabpanel" aria-labelledby="sankey-tab">
@@ -96,14 +126,20 @@
<label for="hideSmallFlows" class="ms-2">Hide Small Flows</label>
</div>
<div class="col-12" id="allTraffic">
<i class="fa fa-spinner fa-spin"></i> Loading Traffic...
<div class="lqos-loading-block lqos-loading-block-lg">
<i class="fa fa-spinner fa-spin"></i>
<span>Loading traffic…</span>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="funnel" role="tabpanel" aria-labelledby="funnel-tab">
<div class="row">
<div class="col-12" id="theFunnel">
<i class="fa fa-spinner fa-spin"></i> Loading Queue Tree...
<div class="lqos-loading-block lqos-loading-block-lg">
<i class="fa fa-spinner fa-spin"></i>
<span>Loading queue tree…</span>
</div>
</div>
</div>
</div>
@@ -129,6 +165,7 @@
</div>
<div class="col-3">
Queue Memory: <span id="cakeQueueMemory">?</span>
<div class="text-muted small mt-1">Queue Type: <span id="cakeQueueType">?</span></div>
</div>
</div>
</div>
@@ -27,8 +27,8 @@
<div class="card-header">Available QoO Profiles</div>
<div class="card-body">
<div id="qooProfilesLoadAlert"></div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<div class="table-responsive lqos-table-wrap">
<table class="lqos-table lqos-table-compact align-middle mb-0">
<thead>
<tr>
<th>Name</th>
@@ -96,7 +96,7 @@
<div class="tab-pane fade show active" id="v-pills-home" role="tabpanel"
aria-labelledby="v-pills-home-tab">
<h2><i class="fa fa-server"></i> General Settings</h2>
<table class="table table-bordered">
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindVersion">Version: </label></td>
<td><input type="text" id="bindVersion" style="background: #ddd"/></td>
@@ -136,7 +136,7 @@
<div class="tab-pane fade" id="v-pills-tuning" role="tabpanel" aria-labelledby="v-pills-tuning-tab">
<h2><i class="fa fa-warning"></i> Tuning Settings</h2>
<table class="table table-bordered">
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindStopIrqBalance">Stop IRQ Balancer: </label></td>
<td><input type="checkbox" id="bindStopIrqBalance"/></td>
@@ -187,7 +187,7 @@
<div id="bridgeMode">
<h3>Bridge Mode</h3>
<table class="table table-bordered">
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindUseXdpBridge">Use XDP Enhanced Bridge: </label></td>
<td><input type="checkbox" id="bindUseXdpBridge"/></td>
@@ -207,7 +207,7 @@
<div id="OnAStickMode">
<h3>Single-Interface ("On a Stick") Mode</h3>
<table class="table table-bordered">
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindSingleInterfaceNic">Network Interface: </label></td>
<td><select id="bindSingleInterfaceNic"></select></td>
@@ -228,7 +228,7 @@
<div class="tab-pane fade" id="v-pills-queues" role="tabpanel" aria-labelledby="v-pills-queues-tab">
<h2><i class="fa car"></i> Queue Settings</h2>
<table class="table table-bordered">
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindSqm">SQM Mode: </label></td>
<td>
@@ -283,7 +283,7 @@
<!-- Long-Term Stats Tab -->
<div class="tab-pane fade" id="v-pills-lts" role="tabpanel" aria-labelledby="v-pills-lts-tab">
<h2><i class="fa fa-line-chart"></i> Insight Settings</h2>
<table class="table table-bordered">
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindEnableLTS">Send Statistics to Insight: </label></td>
<td><input type="checkbox" id="bindEnableLTS"/></td>
@@ -327,7 +327,7 @@
<!-- Flows Tab -->
<div class="tab-pane fade" id="v-pills-flows" role="tabpanel" aria-labelledby="v-pills-flow-tab">
<h2><i class="fa fa-arrow-circle-down"></i> Flows Settings</h2>
<table class="table table-bordered">
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindFlowsTimeout">Flow Timeout (Seconds): </label></td>
<td><input type="number" id="bindFlowsTimeout"/></td>
@@ -359,7 +359,7 @@
<!-- Integration Common Tab -->
<div class="tab-pane fade" id="v-pills-icommon" role="tabpanel" aria-labelledby="v-pills-icommon-tab">
<h2><i class="fa fa-link"></i> Common Integration Settings</h2>
<table class="table table-bordered">
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindCircuitNameAsAddress">Use Circuit Name as Address: </label></td>
<td><input type="checkbox" id="bindCircuitNameAsAddress"/></td>
@@ -382,7 +382,7 @@
<!-- Splynx Tab -->
<div class="tab-pane fade" id="v-pills-splynx" role="tabpanel" aria-labelledby="v-pills-splynx-tab">
<h2><i class="fa fa-link"></i> Splynx Integration Settings</h2>
<table class="table table-bordered">
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindSplynxEnable">Enable Splynx Integration: </label></td>
<td><input type="checkbox" id="bindSplynxEnable"/></td>
@@ -405,7 +405,7 @@
<!-- Netzur Tab -->
<div class="tab-pane fade" id="v-pills-netzur" role="tabpanel" aria-labelledby="v-pills-netzur-tab">
<h2><i class="fa fa-link"></i> Netzur Integration Settings</h2>
<table class="table table-bordered">
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindNetzurEnable">Enable Netzur Integration: </label></td>
<td><input type="checkbox" id="bindNetzurEnable"/></td>
@@ -428,7 +428,7 @@
<!-- UISP Tab -->
<div class="tab-pane fade" id="v-pills-uisp" role="tabpanel" aria-labelledby="v-pills-uisp-tab">
<h2><i class="fa fa-link"></i> UISP Integration Settings</h2>
<table class="table table-bordered">
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindUispEnable">Enable UISP Integration: </label></td>
<td><input type="checkbox" id="bindUispEnable"/></td>
@@ -506,7 +506,7 @@
<div class="tab-pane fade" id="v-pills-powercode" role="tabpanel"
aria-labelledby="v-pills-powercode-tab">
<h2><i class="fa fa-link"></i> Powercode Integration Settings</h2>
<table class="table table-bordered">
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindPowercodeEnable">Enable Powercode Integration: </label></td>
<td><input type="checkbox" id="bindPowercodeEnable"/></td>
@@ -525,7 +525,7 @@
<!-- Sonar Tab -->
<div class="tab-pane fade" id="v-pills-sonar" role="tabpanel" aria-labelledby="v-pills-sonar-tab">
<h2><i class="fa fa-link"></i> Sonar Integration Settings</h2>
<table class="table table-bordered">
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindSonarEnable">Enable Sonar Integration: </label></td>
<td><input type="checkbox" id="bindSonarEnable"/></td>
@@ -560,7 +560,7 @@
<!-- InfluxDB Tab -->
<div class="tab-pane fade" id="v-pills-influxdb" role="tabpanel" aria-labelledby="v-pills-influxdb-tab">
<h2><i class="fa fa-link"></i> InfluxDB Integration Settings</h2>
<table class="table table-bordered">
<table class="lqos-table lqos-table-compact">
<tr>
<td><label for="bindInfluxEnable">Enable InfluxDB Integration: </label></td>
<td><input type="checkbox" id="bindInfluxEnable"/></td>
@@ -258,6 +258,29 @@ html[data-bs-theme="light"] .sidebar .navbar-toggler-icon {
color: var(--bs-secondary-color);
text-decoration: none;
}
.dashbox-body-scroll {
overflow-y: auto;
}
.dashbox-body-scroll-top10 {
min-height: 25rem;
max-height: 25rem;
}
@media (max-width: 1399.98px) {
.dashbox-body-scroll-top10 {
min-height: 22rem;
max-height: 22rem;
}
}
@media (max-width: 991.98px) {
.dashbox-body-scroll-top10 {
min-height: 18rem;
max-height: 18rem;
}
}
.dashbox .dashbox-title a:hover {
color: var(--bs-body-color);
}
@@ -279,6 +302,7 @@ html[data-bs-theme="light"] .sidebar .navbar-toggler-icon {
.btn:hover {
transform: translateY(-1px);
}
.lqos-pill-tabs,
#dashboard .nav-tabs {
border-bottom: 0;
gap: 0.5rem;
@@ -286,13 +310,16 @@ html[data-bs-theme="light"] .sidebar .navbar-toggler-icon {
overflow-x: auto;
padding-bottom: 0.25rem;
}
.lqos-pill-tabs::-webkit-scrollbar,
#dashboard .nav-tabs::-webkit-scrollbar {
height: 6px;
}
.lqos-pill-tabs::-webkit-scrollbar-thumb,
#dashboard .nav-tabs::-webkit-scrollbar-thumb {
background: var(--lqos-border);
border-radius: 999px;
}
.lqos-pill-tabs .nav-link,
#dashboard .nav-tabs .nav-link {
border: 1px solid transparent;
border-radius: 999px;
@@ -300,10 +327,12 @@ html[data-bs-theme="light"] .sidebar .navbar-toggler-icon {
color: var(--bs-secondary-color);
background: transparent;
}
.lqos-pill-tabs .nav-link:hover,
#dashboard .nav-tabs .nav-link:hover {
background: var(--lqos-hover-bg);
color: var(--bs-body-color);
}
.lqos-pill-tabs .nav-link.active,
#dashboard .nav-tabs .nav-link.active {
background: var(--lqos-surface-solid);
border-color: var(--lqos-border);
@@ -318,8 +347,86 @@ html[data-bs-theme="light"] .sidebar .navbar-toggler-icon {
width: 100%;
height: 250px;
}
.lqos-loading-block {
min-height: 220px;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 1rem;
border: 1px solid var(--lqos-border);
border-radius: var(--lqos-radius-md);
background: linear-gradient(
180deg,
color-mix(in srgb, var(--lqos-surface-2) 88%, transparent) 0%,
var(--lqos-surface-solid) 100%
);
color: var(--bs-secondary-color);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.lqos-loading-block.lqos-loading-block-sm {
min-height: 140px;
}
.lqos-loading-block.lqos-loading-block-lg {
min-height: 320px;
}
.lqos-loading-block i {
font-size: 1rem;
}
.lqos-circuit-top-row {
--bs-gutter-x: 1rem;
--bs-gutter-y: 1rem;
}
.lqos-circuit-panel {
height: 100%;
min-height: 230px;
border: 1px solid var(--lqos-border);
border-radius: var(--lqos-radius-md);
background: linear-gradient(
180deg,
color-mix(in srgb, var(--lqos-surface-2) 88%, transparent) 0%,
var(--lqos-surface-solid) 100%
);
box-shadow: var(--lqos-shadow-sm);
padding: 0.85rem;
}
.lqos-circuit-panel h5 {
display: flex;
align-items: center;
gap: 0.45rem;
min-width: 0;
}
.lqos-circuit-panel h5 .fa {
flex: 0 0 auto;
}
.lqos-circuit-panel .lqos-table-wrap {
height: calc(100% - 2rem);
}
.lqos-circuit-panel .lqos-table {
height: 100%;
}
.lqos-circuit-gauge-panel {
display: flex;
align-items: center;
justify-content: center;
}
.lqos-circuit-gauge-grid {
width: 100%;
display: grid;
grid-template-columns: 2fr 1fr;
align-items: center;
gap: 0.25rem;
}
.lqos-circuit-chart-panel {
display: flex;
align-items: center;
}
.lqos-circuit-chart-panel > div {
width: 100%;
height: 200px;
}
.tiny {
font-size: 6pt;
font-size: 0.72rem;
}
.darkenBackground {
z-index: 200;
@@ -363,6 +470,155 @@ h5 {
color: var(--bs-secondary-color);
}
.lqos-table-wrap {
width: 100%;
overflow: auto;
border: 1px solid var(--lqos-border);
border-radius: var(--lqos-radius-md);
background: linear-gradient(
180deg,
color-mix(in srgb, var(--lqos-surface-2) 88%, transparent) 0%,
var(--lqos-surface-solid) 100%
);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.lqos-table,
.dash-table {
width: 100%;
margin: 0;
border-collapse: separate;
border-spacing: 0;
color: var(--bs-body-color);
}
.lqos-table thead th,
.dash-table thead th {
padding: 0.75rem 0.85rem;
border-bottom: 1px solid var(--lqos-border);
background: color-mix(in srgb, var(--lqos-surface-2) 92%, transparent);
color: var(--bs-secondary-color);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
white-space: nowrap;
}
.lqos-table tbody td,
.dash-table tbody td {
padding: 0.72rem 0.85rem;
border-top: 1px solid color-mix(in srgb, var(--lqos-border) 72%, transparent);
color: var(--bs-body-color);
vertical-align: middle;
}
.lqos-table tbody tr:first-child td,
.dash-table tbody tr:first-child td {
border-top: 0;
}
.lqos-table tbody tr:nth-child(2n) td,
.dash-table tbody tr:nth-child(2n) td {
background: color-mix(in srgb, var(--lqos-surface-2) 56%, transparent);
}
.lqos-table tbody tr:hover td,
.dash-table tbody tr:hover td {
background: color-mix(in srgb, var(--lqos-hover-bg) 88%, transparent);
}
.lqos-table a,
.dash-table a {
color: var(--bs-body-color);
text-decoration-line: underline;
text-decoration-thickness: 0.08em;
text-underline-offset: 0.14em;
text-decoration-color: color-mix(in srgb, currentColor 58%, transparent);
}
.lqos-table a:hover,
.dash-table a:hover {
color: var(--bs-info);
text-decoration-color: currentColor;
}
.lqos-table.lqos-table-compact thead th,
.dash-table.lqos-table-compact thead th {
padding: 0.55rem 0.7rem;
}
.lqos-table.lqos-table-compact tbody td,
.dash-table.lqos-table-compact tbody td {
padding: 0.55rem 0.7rem;
}
.lqos-table.lqos-table-tight thead th,
.dash-table.lqos-table-tight thead th {
padding: 0.45rem 0.6rem;
}
.lqos-table.lqos-table-tight tbody td,
.dash-table.lqos-table-tight tbody td {
padding: 0.45rem 0.6rem;
}
.lqos-table.lqos-table-tight {
font-size: 0.9rem;
}
.lqos-table .table-label-cell,
.dash-table .table-label-cell {
width: 40%;
color: var(--bs-secondary-color);
font-weight: 600;
}
.lqos-table .table-value-cell,
.dash-table .table-value-cell {
color: var(--bs-body-color);
}
.lqos-table .muted-meta,
.dash-table .muted-meta {
color: var(--bs-tertiary-color);
}
.lqos-rtt-metric-cell {
min-width: 9.5rem;
}
.lqos-rtt-metric {
min-height: 2.4rem;
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.18rem;
}
.lqos-rtt-metric-line {
display: flex;
align-items: baseline;
gap: 0.35rem;
white-space: nowrap;
}
.lqos-rtt-metric-label {
color: var(--bs-secondary-color);
font-size: 0.72rem;
font-weight: 600;
}
.lqos-rtt-metric-value {
font-size: 0.72rem;
font-variant-numeric: tabular-nums;
}
.lqos-rtt-metric-line.text-secondary .lqos-rtt-metric-value {
color: inherit;
}
.lqos-ellipsis-inline {
display: inline-block;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
.lqos-table-cell-ellipsis {
display: block;
max-width: 12rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.lqos-asn-cell {
width: 11rem;
max-width: 11rem;
}
.lqos-asn-cell .lqos-table-cell-ellipsis {
max-width: 100%;
}
/* Funky tricks for the tree view */
.overlayThroughputWrapper {
position: relative;
@@ -410,9 +666,6 @@ h5 {
box-shadow: var(--lqos-shadow-md);
background: var(--lqos-surface-solid);
}
.dash-table {
width: 100%;
}
.fa-centerline {
vertical-align: middle;
}
@@ -428,6 +681,18 @@ table tr td {
table tr td a {
color: var(--bs-secondary-color);
}
.lqos-table>:not(caption)>*>*,
.dash-table>:not(caption)>*>* {
color: inherit;
}
.lqos-table thead,
.dash-table thead,
.lqos-table tr td,
.dash-table tr td,
.lqos-table tr td a,
.dash-table tr td a {
color: inherit;
}
/* For the config panel */
@@ -18,9 +18,9 @@
<h6 class="mb-0 text-secondary"><i class="fa fa-tachometer-alt me-2 text-primary"></i>Current limits</h6>
<small class="text-muted">StormguardStatus</small>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead class="table-light">
<div class="table-responsive lqos-table-wrap">
<table class="lqos-table lqos-table-compact align-middle mb-0">
<thead>
<tr>
<th scope="col"><i class="fa fa-sitemap me-1"></i>Site</th>
<th scope="col"><i class="fa fa-arrow-down me-1 text-primary"></i>Download (Mbps)</th>
@@ -43,9 +43,9 @@
<h6 class="mb-0 text-secondary"><i class="fa fa-microscope me-2 text-warning"></i>Evaluation data</h6>
<small class="text-muted">StormguardDebug</small>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead class="table-light">
<div class="table-responsive lqos-table-wrap">
<table class="lqos-table lqos-table-compact align-middle mb-0">
<thead>
<tr>
<th scope="col">Site</th>
<th scope="col">Dir</th>
@@ -1,40 +1,42 @@
<div class="row">
<div class="col-4">
<h5><i class="fa fa-tree"></i> Network Tree (<span id="nodeName" class="redactable"></span>)</h5>
<table class="table table-sm">
<thead>
<th></th>
<th>Download</th>
<th>Upload</th>
</thead>
<tbody>
<tr class="small">
<td style="font-weight: bold;">Limits:</td>
<td class="small"><span id="parentLimitsD"></span></td>
<td class="small"><span id="parentLimitsU"></span></td>
</tr>
<tr class="small">
<td style="font-weight: bold;">Throughput:</td>
<td><span id="parentTpD"></span></td>
<td><span id="parentTpU"></span></td>
</tr>
<tr class="small">
<td style="font-weight: bold;">RTT:</td>
<td><span id="parentRttD"></span></td>
<td><span id="parentRttU"></span></td>
</tr>
<tr class="small">
<td style="font-weight: bold;">QoO:</td>
<td><span id="parentQooD"></span></td>
<td><span id="parentQooU"></span></td>
</tr>
<tr class="small">
<td style="font-weight: bold;">TCP Retransmits:</td>
<td><span id="parentRxmitD"></span></td>
<td><span id="parentRxmitU"></span></td>
</tr>
</tbody>
</table>
<div class="lqos-table-wrap">
<table class="lqos-table lqos-table-compact">
<thead>
<th></th>
<th>Download</th>
<th>Upload</th>
</thead>
<tbody>
<tr class="small">
<td class="table-label-cell">Limits</td>
<td class="small"><span id="parentLimitsD"></span></td>
<td class="small"><span id="parentLimitsU"></span></td>
</tr>
<tr class="small">
<td class="table-label-cell">Throughput</td>
<td><span id="parentTpD"></span></td>
<td><span id="parentTpU"></span></td>
</tr>
<tr class="small">
<td class="table-label-cell">RTT</td>
<td><span id="parentRttD"></span></td>
<td><span id="parentRttU"></span></td>
</tr>
<tr class="small">
<td class="table-label-cell">QoO</td>
<td><span id="parentQooD"></span></td>
<td><span id="parentQooU"></span></td>
</tr>
<tr class="small">
<td class="table-label-cell">TCP Retransmits</td>
<td><span id="parentRxmitD"></span></td>
<td><span id="parentRxmitU"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="row">