Improve dynamic circuits config UX

- Rework Operation layout to avoid wide-table squeeze\n- Add responsive config table styling\n- Collapse min/max speed columns into down/up pairs
This commit is contained in:
Herbert Wolverson committed 2026-04-13 11:44:34 -05:00
1 parent 6e4bb732ed
commit 5a1e266dd0
3 files changed
+157 -49

No files matched your search

@@ -119,7 +119,7 @@ function renderRangesTable() {
if (!Array.isArray(ranges) || ranges.length === 0) {
const empty = document.createElement("tr");
const cell = document.createElement("td");
cell.colSpan = 8;
cell.colSpan = 6;
cell.className = "text-muted";
cell.textContent = "No rules configured.";
empty.appendChild(cell);
@@ -150,49 +150,91 @@ function renderRangesTable() {
return input;
};
const addCell = (child) => {
const mkRatePair = (
downValue,
onDownChange,
upValue,
onUpChange,
) => {
const container = document.createElement("div");
container.className = "d-grid gap-1";
const mkDir = (icon, ariaLabel, value, onChange) => {
const group = document.createElement("div");
group.className = "input-group input-group-sm";
const prefix = document.createElement("span");
prefix.className = "input-group-text";
const i = document.createElement("i");
i.className = `fa ${icon}`;
prefix.appendChild(i);
const input = mkNumber(value, onChange);
input.setAttribute("aria-label", ariaLabel);
group.appendChild(prefix);
group.appendChild(input);
return group;
};
container.appendChild(mkDir("fa-arrow-down", "Down Mbps", downValue, onDownChange));
container.appendChild(mkDir("fa-arrow-up", "Up Mbps", upValue, onUpChange));
return container;
};
const addCell = (label, child) => {
const td = document.createElement("td");
td.dataset.label = label;
td.appendChild(child);
tr.appendChild(td);
};
addCell(
"Name",
mkText(rule.name, (v) => {
window.config.dynamic_circuits.ranges[index].name = v;
}, "Rule name"),
);
addCell(
"IP Range",
mkText(rule.ip_range, (v) => {
window.config.dynamic_circuits.ranges[index].ip_range = v;
}, "192.168.0.0/24"),
);
addCell(
mkNumber(rule.download_min_mbps, (v) => {
window.config.dynamic_circuits.ranges[index].download_min_mbps = v;
}),
"Min Mbps",
mkRatePair(
rule.download_min_mbps,
(v) => {
window.config.dynamic_circuits.ranges[index].download_min_mbps = v;
},
rule.upload_min_mbps,
(v) => {
window.config.dynamic_circuits.ranges[index].upload_min_mbps = v;
},
),
);
addCell(
mkNumber(rule.upload_min_mbps, (v) => {
window.config.dynamic_circuits.ranges[index].upload_min_mbps = v;
}),
);
addCell(
mkNumber(rule.download_max_mbps, (v) => {
window.config.dynamic_circuits.ranges[index].download_max_mbps = v;
}),
);
addCell(
mkNumber(rule.upload_max_mbps, (v) => {
window.config.dynamic_circuits.ranges[index].upload_max_mbps = v;
}),
"Max Mbps",
mkRatePair(
rule.download_max_mbps,
(v) => {
window.config.dynamic_circuits.ranges[index].download_max_mbps = v;
},
rule.upload_max_mbps,
(v) => {
window.config.dynamic_circuits.ranges[index].upload_max_mbps = v;
},
),
);
const attach = mkText(rule.attach_to, (v) => {
window.config.dynamic_circuits.ranges[index].attach_to = v;
}, "network.json node (optional)");
attach.setAttribute("list", "networkNodesDatalist");
addCell(attach);
addCell("Attach To", attach);
const removeTd = document.createElement("td");
removeTd.dataset.label = "Actions";
const removeBtn = document.createElement("button");
removeBtn.type = "button";
removeBtn.className = "btn btn-sm btn-outline-danger";
@@ -13,21 +13,28 @@
</div>
<div class="row g-3">
<div class="col-12 col-xl-6">
<div class="lqos-config-section h-100">
<div class="col-12">
<div class="lqos-config-section">
<h6 class="lqos-config-section-title">Operation</h6>
<div class="lqos-config-section-subtitle">Enable the subsystem and choose retention and promotion behavior.</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="dynamicCircuitsEnabled">
<label class="form-check-label" for="dynamicCircuitsEnabled">Enable Dynamic Circuits</label>
<div class="form-text">Master switch for dynamic circuit tracking and promotion.</div>
</div>
<div class="row g-3">
<div class="col-12 col-xl-6">
<div class="mb-0 form-check form-switch">
<input type="checkbox" class="form-check-input" id="dynamicCircuitsEnabled">
<label class="form-check-label" for="dynamicCircuitsEnabled">Enable Dynamic Circuits</label>
<div class="form-text">Master switch for dynamic circuit tracking and promotion.</div>
</div>
</div>
<div class="mb-3">
<label for="dynamicCircuitsTtlSeconds" class="form-label">Time to Live (seconds)</label>
<input type="number" class="form-control" id="dynamicCircuitsTtlSeconds" min="1" step="1" value="300">
<div class="form-text">How long dynamic circuit state remains valid without being seen.</div>
<div class="col-12 col-xl-6">
<label for="dynamicCircuitsTtlSeconds" class="form-label">Time to Live</label>
<div class="input-group">
<input type="number" class="form-control" id="dynamicCircuitsTtlSeconds" min="1" step="1" value="300">
<span class="input-group-text">seconds</span>
</div>
<div class="form-text">How long dynamic circuit state remains valid without being seen.</div>
</div>
</div>
</div>
</div>
@@ -37,37 +44,48 @@
<h6 class="lqos-config-section-title">Range Rules</h6>
<div class="lqos-config-section-subtitle">Define the IP networks that should receive default rates and an optional topology attachment.</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="unknownIpPromotionEnabled">
<label class="form-check-label" for="unknownIpPromotionEnabled">Enable Unknown IP Promotion</label>
<div class="form-text">When enabled, unknown IP traffic is evaluated against the range rules below.</div>
<div class="d-flex align-items-start justify-content-between flex-wrap gap-3 mb-3">
<div class="form-check form-switch mb-0">
<input type="checkbox" class="form-check-input" id="unknownIpPromotionEnabled">
<label class="form-check-label" for="unknownIpPromotionEnabled">Enable Unknown IP Promotion</label>
<div class="form-text">When enabled, unknown IP traffic is evaluated against the range rules below.</div>
</div>
<div class="d-flex align-items-center gap-2">
<button type="button" id="addDynamicRange" class="btn btn-outline-primary btn-sm">
<i class="fa fa-plus me-1"></i>Add Rule
</button>
</div>
</div>
<datalist id="networkNodesDatalist"></datalist>
<div class="table-responsive">
<table class="table table-sm align-middle">
<table class="table table-sm table-hover align-middle lqos-config-responsive-table">
<thead>
<tr>
<th style="min-width: 12rem;">Name</th>
<th style="min-width: 12rem;">IP Range</th>
<th style="min-width: 8rem;">Min Down</th>
<th style="min-width: 8rem;">Min Up</th>
<th style="min-width: 8rem;">Max Down</th>
<th style="min-width: 8rem;">Max Up</th>
<th style="min-width: 12rem;">Attach To</th>
<th style="min-width: 6rem;">Actions</th>
<th style="min-width: 9rem;">Name</th>
<th style="min-width: 10rem;">
IP Range
<span
class="ms-1 text-muted"
role="button"
tabindex="0"
aria-label="IP range help"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="CIDR notation (for example 192.168.200.0/24). 0.0.0.0 and :: mean match-all."
><i class="fas fa-info-circle"></i></span>
</th>
<th style="min-width: 9rem;">Min Mbps</th>
<th style="min-width: 9rem;">Max Mbps</th>
<th style="min-width: 11rem;">Attach To</th>
<th style="min-width: 5rem;">Actions</th>
</tr>
</thead>
<tbody id="dynamicRangesBody"></tbody>
</table>
</div>
<button type="button" id="addDynamicRange" class="btn btn-outline-primary btn-sm">Add Rule</button>
<div class="form-text mt-2">
IP Range accepts CIDR notation (for example <code>192.168.200.0/24</code>). <code>0.0.0.0</code> and <code>::</code> are allowed and mean match-all.
</div>
</div>
</div>
</div>
@@ -1903,6 +1903,54 @@ table tr td a {
color: var(--bs-secondary-color) !important;
}
/* Responsive inline tables for config editors */
@media (max-width: 767.98px) {
.lqos-config-responsive-table thead {
display: none;
}
.lqos-config-responsive-table tbody tr {
display: block;
border: 1px solid var(--lqos-border);
border-radius: var(--lqos-radius-md);
background: color-mix(in srgb, var(--lqos-surface) 70%, transparent);
padding: 0.65rem 0.75rem;
margin-bottom: 0.75rem;
}
.lqos-config-responsive-table tbody td {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.35rem 0;
border: 0;
}
.lqos-config-responsive-table tbody td::before {
content: attr(data-label);
flex: 0 0 7.25rem;
color: var(--bs-secondary-color);
font-weight: 600;
font-size: 0.85rem;
}
.lqos-config-responsive-table tbody td > * {
flex: 1 1 auto;
min-width: 0;
}
.lqos-config-responsive-table tbody td[colspan] {
display: block;
padding: 0.25rem 0;
}
.lqos-config-responsive-table tbody td[colspan]::before {
content: "";
}
}
.lqos-config-network-editor {
display: grid;
gap: 0.9rem;