diff --git a/src/rust/lqosd/src/node_manager/js_build/src/circuit.js b/src/rust/lqosd/src/node_manager/js_build/src/circuit.js index 7a6f5c85..2e93c16c 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/circuit.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/circuit.js @@ -1580,6 +1580,38 @@ let topAsnSortColumn = 'rate'; let topAsnSortDirection = 'desc'; let trafficCurrentPage = 1; let trafficPageSize = DEFAULT_TRAFFIC_PAGE_SIZE; +const TOP_ASN_TABLE_COLUMNS = [ + { group: "attribution" }, + { group: "attribution" }, + { group: "rates", numeric: true }, + { group: "rates", numeric: true }, + { group: "quality", numeric: true }, + { group: "quality", numeric: true }, + { group: "quality", numeric: true }, + { group: "quality", numeric: true }, + { group: "quality", numeric: true }, + { group: "quality", numeric: true }, + { group: "flows", numeric: true }, +]; +const TRAFFIC_TABLE_COLUMNS = [ + { group: "identity" }, + { group: "rates", numeric: true }, + { group: "rates", numeric: true }, + { group: "volume", numeric: true }, + { group: "volume", numeric: true }, + { group: "volume", numeric: true }, + { group: "volume", numeric: true }, + { group: "quality", numeric: true }, + { group: "quality", numeric: true }, + { group: "quality", numeric: true }, + { group: "quality", numeric: true }, + { group: "quality", numeric: true }, + { group: "quality", numeric: true }, + { group: "attribution" }, + { group: "attribution" }, + { group: "attribution" }, + { group: "actions", action: true }, +]; function formatQooScore(score0to100, fallback = "-") { if (score0to100 === null || score0to100 === undefined) { @@ -1630,6 +1662,106 @@ function truncatedTrafficCell(text, cellClass) { return td; } +function applyCircuitTableCellChrome(cell, columns, startIndex, endIndex) { + const descriptor = columns[startIndex]; + if (!descriptor) { + return; + } + + const group = String(descriptor.group || "default"); + cell.classList.add("lqos-circuit-table-cell", `lqos-circuit-colgroup-${group}`); + if (descriptor.numeric) { + cell.classList.add("lqos-circuit-cell-numeric"); + } + if (descriptor.action) { + cell.classList.add("lqos-circuit-cell-action"); + } + + if (startIndex > 0 && columns[startIndex - 1]?.group !== group) { + cell.classList.add("lqos-circuit-col-start"); + } + if (endIndex < columns.length - 1 && columns[endIndex + 1]?.group !== group) { + cell.classList.add("lqos-circuit-col-end"); + } + + cell.dataset.columnStart = String(startIndex); + cell.dataset.columnEnd = String(endIndex); +} + +function annotateCircuitDataTable(table, columns) { + const headRow = table.tHead?.rows?.[0]; + if (headRow) { + let columnIndex = 0; + Array.from(headRow.cells).forEach((cell) => { + const span = Math.max(1, Number(cell.colSpan) || 1); + const startIndex = columnIndex; + const endIndex = Math.min(columns.length - 1, startIndex + span - 1); + applyCircuitTableCellChrome(cell, columns, startIndex, endIndex); + columnIndex += span; + }); + } + + Array.from(table.tBodies).forEach((tbody) => { + Array.from(tbody.rows).forEach((row) => { + if (row.cells.length !== columns.length) { + return; + } + Array.from(row.cells).forEach((cell, index) => { + applyCircuitTableCellChrome(cell, columns, index, index); + }); + }); + }); + + wireCircuitDataTableHover(table); +} + +function wireCircuitDataTableHover(table) { + let activeStart = null; + let activeEnd = null; + + const paintRange = (start, end) => { + Array.from(table.querySelectorAll("[data-column-start][data-column-end]")).forEach((cell) => { + const cellStart = Number.parseInt(cell.dataset.columnStart || "", 10); + const cellEnd = Number.parseInt(cell.dataset.columnEnd || "", 10); + const shouldHighlight = Number.isFinite(cellStart) + && Number.isFinite(cellEnd) + && cellStart <= end + && cellEnd >= start; + cell.classList.toggle("is-column-hover", shouldHighlight); + }); + }; + + const clearRange = () => { + activeStart = null; + activeEnd = null; + Array.from(table.querySelectorAll(".is-column-hover")).forEach((cell) => { + cell.classList.remove("is-column-hover"); + }); + }; + + table.addEventListener("pointerover", (event) => { + const cell = event.target.closest("th, td"); + if (!cell || !table.contains(cell)) { + return; + } + const start = Number.parseInt(cell.dataset.columnStart || "", 10); + const end = Number.parseInt(cell.dataset.columnEnd || "", 10); + if (!Number.isFinite(start) || !Number.isFinite(end)) { + return; + } + if (activeStart === start && activeEnd === end) { + return; + } + activeStart = start; + activeEnd = end; + paintRange(start, end); + }); + + table.addEventListener("pointerleave", () => { + clearRange(); + }); +} + function visibleTrafficRows() { return Array.isArray(latestTrafficPage?.rows) ? latestTrafficPage.rows : []; } @@ -1735,7 +1867,7 @@ function renderTopAsnTab() { tableWrap.classList.add("lqos-table-wrap"); const table = document.createElement("table"); - table.classList.add("lqos-table", "lqos-table-tight", "lqos-circuit-traffic-table"); + table.classList.add("lqos-table", "lqos-table-tight", "lqos-circuit-traffic-table", "lqos-circuit-data-table"); const thead = document.createElement("thead", "small"); thead.style.fontSize = "0.8em"; @@ -1797,6 +1929,7 @@ function renderTopAsnTab() { } table.appendChild(tbody); + annotateCircuitDataTable(table, TOP_ASN_TABLE_COLUMNS); tableWrap.appendChild(table); clearDiv(target); target.appendChild(tableWrap); @@ -1818,7 +1951,7 @@ function renderTrafficTab() { tableWrap.classList.add("lqos-table-wrap"); let table = document.createElement("table"); - table.classList.add("lqos-table", "lqos-table-tight", "lqos-circuit-traffic-table"); + table.classList.add("lqos-table", "lqos-table-tight", "lqos-circuit-traffic-table", "lqos-circuit-data-table"); let thead = document.createElement("thead", "small"); thead.style.fontSize = "0.8em"; @@ -1903,6 +2036,7 @@ function renderTrafficTab() { } table.appendChild(tbody); + annotateCircuitDataTable(table, TRAFFIC_TABLE_COLUMNS); tableWrap.appendChild(table); clearDiv(target); target.appendChild(tableWrap); 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 4ec8c8c1..8d0aa92c 100644 --- a/src/rust/lqosd/src/node_manager/static2/node_manager.css +++ b/src/rust/lqosd/src/node_manager/static2/node_manager.css @@ -1567,6 +1567,74 @@ h5 { max-width: 100%; } +.lqos-circuit-data-table th, +.lqos-circuit-data-table td { + --lqos-circuit-group-tint: transparent; + --lqos-circuit-hover-tint: transparent; + background-image: + linear-gradient(var(--lqos-circuit-hover-tint), var(--lqos-circuit-hover-tint)), + linear-gradient(var(--lqos-circuit-group-tint), var(--lqos-circuit-group-tint)); + background-clip: padding-box; + transition: + background-color 140ms ease, + border-color 140ms ease, + box-shadow 140ms ease; +} + +.lqos-circuit-data-table thead th + th, +.lqos-circuit-data-table tbody td + td { + border-left: 1px solid color-mix(in srgb, var(--lqos-border) 42%, transparent); +} + +.lqos-circuit-data-table .lqos-circuit-col-start { + border-left-color: color-mix(in srgb, var(--lqos-border) 92%, transparent); +} + +.lqos-circuit-data-table .lqos-circuit-colgroup-identity, +.lqos-circuit-data-table .lqos-circuit-colgroup-actions { + --lqos-circuit-group-tint: color-mix(in srgb, var(--lqos-surface) 14%, transparent); +} + +.lqos-circuit-data-table .lqos-circuit-colgroup-rates { + --lqos-circuit-group-tint: color-mix(in srgb, var(--bs-info) 7%, transparent); +} + +.lqos-circuit-data-table .lqos-circuit-colgroup-volume { + --lqos-circuit-group-tint: color-mix(in srgb, #d39f37 7%, transparent); +} + +.lqos-circuit-data-table .lqos-circuit-colgroup-quality { + --lqos-circuit-group-tint: color-mix(in srgb, #2b8a57 7%, transparent); +} + +.lqos-circuit-data-table .lqos-circuit-colgroup-attribution, +.lqos-circuit-data-table .lqos-circuit-colgroup-flows { + --lqos-circuit-group-tint: color-mix(in srgb, var(--bs-secondary-color) 8%, transparent); +} + +.lqos-circuit-data-table .lqos-circuit-cell-numeric { + text-align: right; + font-variant-numeric: tabular-nums; +} + +.lqos-circuit-data-table .lqos-circuit-cell-action { + text-align: center; +} + +.lqos-circuit-data-table .is-column-hover { + --lqos-circuit-hover-tint: rgba(255, 255, 255, 0.035); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.05), + inset 0 0 0 1px color-mix(in srgb, var(--lqos-border) 70%, transparent); +} + +[data-bs-theme="light"] .lqos-circuit-data-table .is-column-hover { + --lqos-circuit-hover-tint: rgba(17, 29, 45, 0.045); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.28), + inset 0 0 0 1px color-mix(in srgb, var(--lqos-border) 74%, transparent); +} + .lqos-tree-stack { display: flex; flex-direction: column;