From 31c0f65c865aca2cbc84e2488ef60f5282a70e3c Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Sun, 29 Mar 2026 09:31:29 +1000 Subject: [PATCH] Hosts page UX: simpler table, projection, and effective-config tweaks Remove the Status column and row-level badges/actions; keep source-level Editable on group headers. Double-click managed rows to edit; commit staged drafts when focus leaves the row via isFocusOutsideElement in the shared scroll-cue module. Hide inactive system-host rows when dnsmasq ignores them and move system-hosts / expand-hosts notes below the content area. Memoize VisibleGroups so BuildGroups runs once per render during search. Move read-ethers metadata to the DHCP section; adjust Hosts page state copy and related effective-config tooltips/builders where needed. --- src/DnsmasqWebUI/Components/Pages/Hosts.razor | 142 ++++++++++-------- .../Components/Pages/Hosts.razor.css | 68 +-------- .../Hosts/HostsPageProjectionService.cs | 42 +----- .../Metadata/DnsmasqOptionTooltips.cs | 6 +- .../Metadata/EffectiveConfigFieldBuilder.cs | 20 +-- .../Metadata/EffectiveConfigSections.cs | 18 +-- .../Metadata/EffectiveConfigViews.cs | 3 +- .../EffectiveConfigRenderFragmentRegistry.cs | 23 ++- .../Dnsmasq/PageStates/HostsPageState.cs | 6 +- .../wwwroot/js/modules/table-scroll-cue.js | 12 ++ 10 files changed, 141 insertions(+), 199 deletions(-) diff --git a/src/DnsmasqWebUI/Components/Pages/Hosts.razor b/src/DnsmasqWebUI/Components/Pages/Hosts.razor index 6c0ece8..515c5ed 100644 --- a/src/DnsmasqWebUI/Components/Pages/Hosts.razor +++ b/src/DnsmasqWebUI/Components/Pages/Hosts.razor @@ -8,6 +8,7 @@ @using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Abstractions @using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Hosts.Abstractions @using DnsmasqWebUI.Components.EffectiveConfig +@using Microsoft.AspNetCore.Components.Web @using Microsoft.JSInterop @implements IDisposable @inject IStatusClient StatusClient @@ -37,13 +38,6 @@ else - @if (!pageState.SystemHostsActive && !string.IsNullOrEmpty(pageState.SystemHostsInactiveReason)) - { -
- System hosts is inactive: @pageState.SystemHostsInactiveReason -
- } - @if (!pageState.ManagedHostsAvailable) {
@@ -97,14 +91,13 @@ else Address Names Effective names - Status @foreach (var group in VisibleGroups) { - +
@group.Title @@ -128,32 +121,25 @@ else { @if (IsEditingRow(row)) { - + - + @string.Join(", ", HostsEffectiveNames.Expand(ParseNamesList(_editNames), pageState.ExpandHostsEnabled, pageState.ExpansionDomain)) - -
- Editable - @if (row.IsComment) - { - Commented - } -
- - -
-
- } else { - + @row.Address @string.Join(", ", row.Names) @@ -166,30 +152,6 @@ else @string.Join(", ", row.EffectiveNames) } - -
- @if (!row.IsActive) - { - Inactive - } - @if (row.IsComment) - { - Commented - } - @if (row.IsEditable) - { - Editable - } - else - { - Read-only - } - @if (row.IsEditable && string.IsNullOrEmpty(_editingRowId)) - { - - } -
- } } @@ -200,17 +162,25 @@ else
} - @if (pageState.ExpandHostsEnabled && !string.IsNullOrEmpty(pageState.ExpansionDomain)) - { -
- expand-hosts is enabled with domain @pageState.ExpansionDomain. - Simple hostnames (without dots) will be expanded to include the domain suffix. - The Effective names column shows how dnsmasq will resolve each entry. -
- } - } + +
+ @if (!pageState.SystemHostsActive && !string.IsNullOrEmpty(pageState.SystemHostsInactiveReason)) + { +
+ System hosts: @pageState.SystemHostsInactiveReason +
+ } + @if (pageState.ExpandHostsEnabled && !string.IsNullOrEmpty(pageState.ExpansionDomain)) + { +
+ expand-hosts is enabled with domain @pageState.ExpansionDomain. + Simple hostnames (without dots) will be expanded to include the domain suffix. + The Effective names column shows how dnsmasq will resolve each entry. +
+ } +
}
@@ -227,10 +197,31 @@ else private string? _editingRowId; private string _editAddress = ""; private string _editNames = ""; + private ElementReference _editingRowRef; private IJSObjectReference? _hostsTableScrollCueModule; - private IReadOnlyList VisibleGroups => - HostsProjection.BuildGroups(_unifiedRows, BuildQuery()); + // Memoize: markup reads VisibleGroups.Count then foreach — avoid building twice per render. + private IReadOnlyList? _visibleGroupsCache; + private object? _visibleGroupsCacheRowsRef; + private string _visibleGroupsCacheSearch = "\0"; + + private IReadOnlyList VisibleGroups + { + get + { + if (_visibleGroupsCache != null + && ReferenceEquals(_visibleGroupsCacheRowsRef, _unifiedRows) + && _visibleGroupsCacheSearch == _search) + { + return _visibleGroupsCache; + } + + _visibleGroupsCache = HostsProjection.BuildGroups(_unifiedRows, BuildQuery()); + _visibleGroupsCacheRowsRef = _unifiedRows; + _visibleGroupsCacheSearch = _search; + return _visibleGroupsCache; + } + } private HostsPageQueryState BuildQuery() { @@ -247,13 +238,15 @@ else private static string RowClass(HostsPageRow row) { var parts = new List { "hosts-row" }; - if (row.IsComment) - parts.Add("table-secondary"); + if (row.IsEditable) + parts.Add("hosts-row--editable"); if (!row.IsActive) parts.Add("hosts-row--inactive"); return string.Join(" ", parts); } + private void OnRowDoubleClick(HostsPageRow row) => BeginEdit(row); + private bool IsEditingRow(HostsPageRow row) => !string.IsNullOrEmpty(_editingRowId) && string.Equals(_editingRowId, row.Id, StringComparison.Ordinal); @@ -276,7 +269,30 @@ else _editNames = ""; } - private async Task SaveEditAsync() + private async Task OnEditingRowFocusOut(FocusEventArgs _) + { + if (string.IsNullOrEmpty(_editingRowId)) + return; + await Task.Delay(10); + if (string.IsNullOrEmpty(_editingRowId)) + return; + + var outside = true; + try + { + if (_hostsTableScrollCueModule != null) + outside = await _hostsTableScrollCueModule.InvokeAsync("isFocusOutsideElement", _editingRowRef); + } + catch (JSDisconnectedException) + { + outside = true; + } + + if (outside) + await CommitAndEndInlineEditAsync(); + } + + private async Task CommitAndEndInlineEditAsync() { if (string.IsNullOrEmpty(_editingRowId) || _status == null) return; diff --git a/src/DnsmasqWebUI/Components/Pages/Hosts.razor.css b/src/DnsmasqWebUI/Components/Pages/Hosts.razor.css index a44f506..232666b 100644 --- a/src/DnsmasqWebUI/Components/Pages/Hosts.razor.css +++ b/src/DnsmasqWebUI/Components/Pages/Hosts.razor.css @@ -139,6 +139,14 @@ opacity: 0.72; } +::deep .hosts-row--editable { + cursor: pointer; +} + +::deep .hosts-row--editable:hover { + background-color: rgba(13, 110, 253, 0.06); +} + ::deep .hosts-row code { font-size: 0.93em; color: #0f172a; @@ -151,66 +159,6 @@ background: #f0f7ff; } -.hosts-status-cell { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0.3rem; -} - -.hosts-status-pill { - display: inline-flex; - align-items: center; - padding: 0.22rem 0.48rem; - border-radius: 999px; - font-size: 0.72rem; - font-weight: 600; - line-height: 1.1; - border: 1px solid transparent; - white-space: nowrap; -} - -.hosts-status-pill--active { - color: #0f5132; - background: #d1e7dd; - border-color: #badbcc; -} - -.hosts-status-pill--inactive { - color: #5c3d00; - background: #fff3cd; - border-color: #ffe69c; -} - -.hosts-status-pill--editable { - color: #084298; - background: #dbeafe; - border-color: #b6d4fe; -} - -.hosts-status-pill--readonly { - color: #475569; - background: #f1f5f9; - border-color: #d8e0ea; -} - -.hosts-status-pill--muted { - color: #495057; - background: #f3f4f6; - border-color: #e5e7eb; -} - -.hosts-inline-action { - margin-left: 0.1rem; -} - -.hosts-status-actions { - display: inline-flex; - flex-wrap: wrap; - gap: 0.3rem; - margin-left: 0.1rem; -} - @media (max-width: 991.98px) { .hosts-controls-bar { padding: 0.75rem; diff --git a/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Hosts/HostsPageProjectionService.cs b/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Hosts/HostsPageProjectionService.cs index f186c25..1e6d46c 100644 --- a/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Hosts/HostsPageProjectionService.cs +++ b/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Hosts/HostsPageProjectionService.cs @@ -31,13 +31,15 @@ public sealed class HostsPageProjectionService : IHostsPageProjectionService if (query.ActiveFilter is { } act) filtered = filtered.Where(r => r.IsActive == act); + // When dnsmasq ignores system hosts (e.g. no-hosts), do not show those rows in the table. + filtered = filtered.Where(r => + !(r.SourceKind == HostsRowSourceKind.SystemHosts && !r.IsActive)); + var list = filtered.ToList(); if (list.Count == 0) return Array.Empty(); - return query.Grouping == HostsGroupingMode.Activity - ? BuildActivityGroups(list, query) - : BuildSourceGroups(list, query); + return BuildSourceGroups(list, query); } private static IReadOnlyList BuildSourceGroups(List rows, HostsPageQueryState query) @@ -76,40 +78,6 @@ public sealed class HostsPageProjectionService : IHostsPageProjectionService return result; } - private static IReadOnlyList BuildActivityGroups(List rows, HostsPageQueryState query) - { - var orderedGroups = rows - .GroupBy(r => r.IsActive) - .OrderByDescending(g => g.Key); - - var result = new List(); - foreach (var g in orderedGroups) - { - var sortedRows = SortRows(g, query.Sort, query.Descending).ToList(); - if (sortedRows.Count == 0) - continue; - - var active = g.Key; - var title = active ? "Active" : "Inactive"; - var subtitle = active - ? "Entries from sources dnsmasq loads for hosts." - : "Entries from sources that are ignored or unavailable (e.g. system hosts under no-hosts)."; - - result.Add(new HostsPageGroup( - Key: active ? "activity:active" : "activity:inactive", - Title: title, - Subtitle: subtitle, - SourceKind: null, - GroupContainsEditableRows: sortedRows.Any(r => r.IsEditable), - IsActive: active, - InactiveReason: null, - VisibleRowCount: sortedRows.Count, - Rows: sortedRows)); - } - - return result; - } - private static IEnumerable SortRows(IEnumerable rows, HostsSortMode sort, bool descending) { IEnumerable ordered = sort switch diff --git a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/DnsmasqOptionTooltips.cs b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/DnsmasqOptionTooltips.cs index c287d43..ae6c6d3 100644 --- a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/DnsmasqOptionTooltips.cs +++ b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/DnsmasqOptionTooltips.cs @@ -150,7 +150,7 @@ public static class DnsmasqOptionTooltips [DnsmasqConfKeys.DnssecCheckUnsigned] = "Treat unsigned zones as bogus.", [DnsmasqConfKeys.DnssecNoTimecheck] = "Do not validate DNSSEC signatures against the current time (e.g. for testing).", [DnsmasqConfKeys.DnssecDebug] = "Log DNSSEC validation failures at debug level.", - [DnsmasqConfKeys.AddCpeId] = "Add CPE-ID to DHCP requests (for identification).", + [DnsmasqConfKeys.AddCpeId] = "Add an identifying string to DNS queries forwarded upstream (EDNS0).", [DnsmasqConfKeys.DnssecTimestamp] = "Path to file for DNSSEC timestamp (e.g. for validation).", [DnsmasqConfKeys.DnssecLimits] = "DNSSEC validation limits (e.g. max signed zones).", [DnsmasqConfKeys.TrustAnchor] = "DNSSEC trust anchors. Can repeat.", @@ -163,8 +163,8 @@ public static class DnsmasqOptionTooltips [DnsmasqConfKeys.MaxTtl] = "Maximum TTL for cached responses (cap on upstream TTL).", [DnsmasqConfKeys.MaxCacheTtl] = "Maximum TTL for positive cache entries.", [DnsmasqConfKeys.MinCacheTtl] = "Minimum TTL for positive cache entries.", - [DnsmasqConfKeys.Dumpfile] = "Dump cache to this file (for debugging).", - [DnsmasqConfKeys.Dumpmask] = "Mask for cache dump (which data to include).", + [DnsmasqConfKeys.Dumpfile] = "Pcap file path for packet capture (debugging).", + [DnsmasqConfKeys.Dumpmask] = "Bitmask selecting which packet types are written to the dump file.", [DnsmasqConfKeys.UseStaleCache] = "Use stale cache when upstream is unavailable. Off, On (key-only), or custom seconds.", [DnsmasqConfKeys.AddMac] = "Add MAC address to DNS queries. Off, On (key-only), or custom value.", [DnsmasqConfKeys.StripMac] = "Strip MAC address information from forwarded DNS queries.", diff --git a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/EffectiveConfigFieldBuilder.cs b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/EffectiveConfigFieldBuilder.cs index 3e25f5b..2bc4035 100644 --- a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/EffectiveConfigFieldBuilder.cs +++ b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/EffectiveConfigFieldBuilder.cs @@ -54,7 +54,8 @@ public sealed class EffectiveConfigFieldBuilder : IEffectiveConfigFieldBuilder list.AddDescriptor(registry, DnsmasqConfKeys.NoHosts, status, s => Config(s)?.NoHosts, s => Sources(s)?.NoHosts, null); list.AddDescriptor(registry, DnsmasqConfKeys.AddnHosts, status, null, null, s => ToValueWithSourceList(s?.AddnHostsPaths, Sources(s)?.AddnHostsPaths)); list.AddDescriptor(registry, DnsmasqConfKeys.Hostsdir, status, s => Config(s)?.HostsdirPath, s => Sources(s)?.HostsdirPath, null); - list.AddDescriptor(registry, DnsmasqConfKeys.ReadEthers, status, s => Config(s)?.ReadEthers, s => Sources(s)?.ReadEthers, null); + list.AddDescriptor(registry, DnsmasqConfKeys.ExpandHosts, status, s => Config(s)?.ExpandHosts, s => Sources(s)?.ExpandHosts, null); + list.AddDescriptor(registry, DnsmasqConfKeys.Domain, status, null, null, Items(ec => ec?.DomainValues, src => src?.DomainValues)); // Resolver / DNS list.AddDescriptor(registry, DnsmasqConfKeys.Server, status, null, null, Items(ec => ec?.ServerValues, src => src?.ServerValues)); @@ -78,7 +79,6 @@ public sealed class EffectiveConfigFieldBuilder : IEffectiveConfigFieldBuilder list.AddDescriptor(registry, DnsmasqConfKeys.StopDnsRebind, status, s => Config(s)?.StopDnsRebind, s => Sources(s)?.StopDnsRebind, null); list.AddDescriptor(registry, DnsmasqConfKeys.RebindLocalhostOk, status, s => Config(s)?.RebindLocalhostOk, s => Sources(s)?.RebindLocalhostOk, null); list.AddDescriptor(registry, DnsmasqConfKeys.ClearOnReload, status, s => Config(s)?.ClearOnReload, s => Sources(s)?.ClearOnReload, null); - list.AddDescriptor(registry, DnsmasqConfKeys.ExpandHosts, status, s => Config(s)?.ExpandHosts, s => Sources(s)?.ExpandHosts, null); list.AddDescriptor(registry, DnsmasqConfKeys.BogusPriv, status, s => Config(s)?.BogusPriv, s => Sources(s)?.BogusPriv, null); list.AddDescriptor(registry, DnsmasqConfKeys.RebindDomainOk, status, null, null, Items(ec => ec?.RebindDomainOkValues, src => src?.RebindDomainOkValues)); list.AddDescriptor(registry, DnsmasqConfKeys.BogusNxdomain, status, null, null, Items(ec => ec?.BogusNxdomainValues, src => src?.BogusNxdomainValues)); @@ -96,10 +96,10 @@ public sealed class EffectiveConfigFieldBuilder : IEffectiveConfigFieldBuilder list.AddDescriptor(registry, DnsmasqConfKeys.NoRoundRobin, status, s => Config(s)?.NoRoundRobin, s => Sources(s)?.NoRoundRobin, null); list.AddDescriptor(registry, DnsmasqConfKeys.DnsForwardMax, status, s => Config(s)?.DnsForwardMax, s => Sources(s)?.DnsForwardMax, null); list.AddDescriptor(registry, DnsmasqConfKeys.ConnmarkAllowlist, status, null, null, Items(ec => ec?.ConnmarkAllowlistValues, src => src?.ConnmarkAllowlistValues)); + list.AddDescriptor(registry, DnsmasqConfKeys.AddCpeId, status, s => Config(s)?.AddCpeId, s => Sources(s)?.AddCpeId, null); list.AddDescriptor(registry, DnsmasqConfKeys.Do0x20Encode, status, s => Config(s)?.Do0x20EncodeState, s => Sources(s)?.Do0x20Encode, null); // DNS records - list.AddDescriptor(registry, DnsmasqConfKeys.Domain, status, null, null, Items(ec => ec?.DomainValues, src => src?.DomainValues)); list.AddDescriptor(registry, DnsmasqConfKeys.Cname, status, null, null, Items(ec => ec?.CnameValues, src => src?.CnameValues)); list.AddDescriptor(registry, DnsmasqConfKeys.MxHost, status, null, null, Items(ec => ec?.MxHostValues, src => src?.MxHostValues)); list.AddDescriptor(registry, DnsmasqConfKeys.Srv, status, null, null, Items(ec => ec?.SrvValues, src => src?.SrvValues)); @@ -121,11 +121,16 @@ public sealed class EffectiveConfigFieldBuilder : IEffectiveConfigFieldBuilder list.AddDescriptor(registry, DnsmasqConfKeys.Selfmx, status, s => Config(s)?.Selfmx, s => Sources(s)?.Selfmx, null); // DHCP + list.AddDescriptor(registry, DnsmasqConfKeys.NoDhcpInterface, status, null, null, Items(ec => ec?.NoDhcpInterfaceValues, src => src?.NoDhcpInterfaceValues)); + list.AddDescriptor(registry, DnsmasqConfKeys.NoDhcpv4Interface, status, null, null, Items(ec => ec?.NoDhcpv4InterfaceValues, src => src?.NoDhcpv4InterfaceValues)); + list.AddDescriptor(registry, DnsmasqConfKeys.NoDhcpv6Interface, status, null, null, Items(ec => ec?.NoDhcpv6InterfaceValues, src => src?.NoDhcpv6InterfaceValues)); + list.AddDescriptor(registry, DnsmasqConfKeys.EnableRa, status, s => Config(s)?.EnableRa, s => Sources(s)?.EnableRa, null); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpAuthoritative, status, s => Config(s)?.DhcpAuthoritative, s => Sources(s)?.DhcpAuthoritative, null); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpRapidCommit, status, s => Config(s)?.DhcpRapidCommit, s => Sources(s)?.DhcpRapidCommit, null); list.AddDescriptor(registry, DnsmasqConfKeys.LeasefileRo, status, s => Config(s)?.LeasefileRo, s => Sources(s)?.LeasefileRo, null); list.AddDescriptor(registry, DnsmasqConfKeys.Leasequery, status, null, null, Items(ec => ec?.LeasequeryValues, src => src?.LeasequeryValues)); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpGenerateNames, status, s => Config(s)?.DhcpGenerateNames, s => Sources(s)?.DhcpGenerateNames, null); + list.AddDescriptor(registry, DnsmasqConfKeys.ReadEthers, status, s => Config(s)?.ReadEthers, s => Sources(s)?.ReadEthers, null); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpBroadcast, status, s => Config(s)?.DhcpBroadcast, s => Sources(s)?.DhcpBroadcast, null); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpSequentialIp, status, s => Config(s)?.DhcpSequentialIp, s => Sources(s)?.DhcpSequentialIp, null); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpIgnoreClid, status, s => Config(s)?.DhcpIgnoreClid, s => Sources(s)?.DhcpIgnoreClid, null); @@ -186,7 +191,6 @@ public sealed class EffectiveConfigFieldBuilder : IEffectiveConfigFieldBuilder list.AddDescriptor(registry, DnsmasqConfKeys.DnssecNoTimecheck, status, s => Config(s)?.DnssecNoTimecheck, s => Sources(s)?.DnssecNoTimecheck, null); list.AddDescriptor(registry, DnsmasqConfKeys.DnssecDebug, status, s => Config(s)?.DnssecDebug, s => Sources(s)?.DnssecDebug, null); list.AddDescriptor(registry, DnsmasqConfKeys.ProxyDnssec, status, s => Config(s)?.ProxyDnssec, s => Sources(s)?.ProxyDnssec, null); - list.AddDescriptor(registry, DnsmasqConfKeys.AddCpeId, status, s => Config(s)?.AddCpeId, s => Sources(s)?.AddCpeId, null); list.AddDescriptor(registry, DnsmasqConfKeys.DnssecTimestamp, status, s => Config(s)?.DnssecTimestamp, s => Sources(s)?.DnssecTimestamp, null); list.AddDescriptor(registry, DnsmasqConfKeys.DnssecLimits, status, s => Config(s)?.DnssecLimits, s => Sources(s)?.DnssecLimits, null); list.AddDescriptor(registry, DnsmasqConfKeys.TrustAnchor, status, null, null, Items(ec => ec?.TrustAnchorValues, src => src?.TrustAnchorValues)); @@ -196,8 +200,6 @@ public sealed class EffectiveConfigFieldBuilder : IEffectiveConfigFieldBuilder list.AddDescriptor(registry, DnsmasqConfKeys.CacheSize, status, s => Config(s)?.CacheSize, s => Sources(s)?.CacheSize, null); list.AddDescriptor(registry, DnsmasqConfKeys.LocalTtl, status, s => Config(s)?.LocalTtl, s => Sources(s)?.LocalTtl, null); list.AddDescriptor(registry, DnsmasqConfKeys.NoNegcache, status, s => Config(s)?.NoNegcache, s => Sources(s)?.NoNegcache, null); - list.AddDescriptor(registry, DnsmasqConfKeys.Dumpfile, status, s => Config(s)?.DumpfilePath, s => Sources(s)?.DumpfilePath, null); - list.AddDescriptor(registry, DnsmasqConfKeys.Dumpmask, status, s => Config(s)?.Dumpmask, s => Sources(s)?.Dumpmask, null); list.AddDescriptor(registry, DnsmasqConfKeys.UseStaleCache, status, s => Config(s)?.UseStaleCache, s => Sources(s)?.UseStaleCache, null); list.AddDescriptor(registry, DnsmasqConfKeys.AddMac, status, s => Config(s)?.AddMac, s => Sources(s)?.AddMac, null); list.AddDescriptor(registry, DnsmasqConfKeys.StripMac, status, s => Config(s)?.StripMac, s => Sources(s)?.StripMac, null); @@ -220,16 +222,14 @@ public sealed class EffectiveConfigFieldBuilder : IEffectiveConfigFieldBuilder list.AddDescriptor(registry, DnsmasqConfKeys.ListenAddress, status, null, null, Items(ec => ec?.ListenAddresses, src => src?.ListenAddresses)); list.AddDescriptor(registry, DnsmasqConfKeys.ExceptInterface, status, null, null, Items(ec => ec?.ExceptInterfaces, src => src?.ExceptInterfaces)); list.AddDescriptor(registry, DnsmasqConfKeys.AuthServer, status, null, null, Items(ec => ec?.AuthServerValues, src => src?.AuthServerValues)); - list.AddDescriptor(registry, DnsmasqConfKeys.NoDhcpInterface, status, null, null, Items(ec => ec?.NoDhcpInterfaceValues, src => src?.NoDhcpInterfaceValues)); - list.AddDescriptor(registry, DnsmasqConfKeys.NoDhcpv4Interface, status, null, null, Items(ec => ec?.NoDhcpv4InterfaceValues, src => src?.NoDhcpv4InterfaceValues)); - list.AddDescriptor(registry, DnsmasqConfKeys.NoDhcpv6Interface, status, null, null, Items(ec => ec?.NoDhcpv6InterfaceValues, src => src?.NoDhcpv6InterfaceValues)); + list.AddDescriptor(registry, DnsmasqConfKeys.Dumpfile, status, s => Config(s)?.DumpfilePath, s => Sources(s)?.DumpfilePath, null); + list.AddDescriptor(registry, DnsmasqConfKeys.Dumpmask, status, s => Config(s)?.Dumpmask, s => Sources(s)?.Dumpmask, null); list.AddDescriptor(registry, DnsmasqConfKeys.PidFile, status, s => Config(s)?.PidFilePath, s => Sources(s)?.PidFilePath, null); list.AddDescriptor(registry, DnsmasqConfKeys.User, status, s => Config(s)?.User, s => Sources(s)?.User, null); list.AddDescriptor(registry, DnsmasqConfKeys.Group, status, s => Config(s)?.Group, s => Sources(s)?.Group, null); list.AddDescriptor(registry, DnsmasqConfKeys.LogFacility, status, s => Config(s)?.LogFacility, s => Sources(s)?.LogFacility, null); list.AddDescriptor(registry, DnsmasqConfKeys.EnableDbus, status, s => Config(s)?.EnableDbus, s => Sources(s)?.EnableDbus, null); list.AddDescriptor(registry, DnsmasqConfKeys.EnableUbus, status, s => Config(s)?.EnableUbus, s => Sources(s)?.EnableUbus, null); - list.AddDescriptor(registry, DnsmasqConfKeys.EnableRa, status, s => Config(s)?.EnableRa, s => Sources(s)?.EnableRa, null); list.AddDescriptor(registry, DnsmasqConfKeys.LogDhcp, status, s => Config(s)?.LogDhcp, s => Sources(s)?.LogDhcp, null); list.AddDescriptor(registry, DnsmasqConfKeys.KeepInForeground, status, s => Config(s)?.KeepInForeground, s => Sources(s)?.KeepInForeground, null); list.AddDescriptor(registry, DnsmasqConfKeys.NoDaemon, status, s => Config(s)?.NoDaemon, s => Sources(s)?.NoDaemon, null); diff --git a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/EffectiveConfigSections.cs b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/EffectiveConfigSections.cs index 1cc0332..385e40c 100644 --- a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/EffectiveConfigSections.cs +++ b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/EffectiveConfigSections.cs @@ -29,6 +29,8 @@ public static class EffectiveConfigSections DnsmasqConfKeys.NoHosts, DnsmasqConfKeys.AddnHosts, DnsmasqConfKeys.Hostsdir, + DnsmasqConfKeys.ExpandHosts, + DnsmasqConfKeys.Domain, ]), new SectionDef(SectionResolver, "Resolver / DNS", [ DnsmasqConfKeys.Server, @@ -52,7 +54,6 @@ public static class EffectiveConfigSections DnsmasqConfKeys.StopDnsRebind, DnsmasqConfKeys.RebindLocalhostOk, DnsmasqConfKeys.ClearOnReload, - DnsmasqConfKeys.ExpandHosts, DnsmasqConfKeys.BogusPriv, DnsmasqConfKeys.RebindDomainOk, DnsmasqConfKeys.BogusNxdomain, @@ -70,10 +71,10 @@ public static class EffectiveConfigSections DnsmasqConfKeys.NoRoundRobin, DnsmasqConfKeys.DnsForwardMax, DnsmasqConfKeys.ConnmarkAllowlist, + DnsmasqConfKeys.AddCpeId, DnsmasqConfKeys.Do0x20Encode, ]), new SectionDef(SectionDnsRecords, "DNS records", [ - DnsmasqConfKeys.Domain, DnsmasqConfKeys.Cname, DnsmasqConfKeys.MxHost, DnsmasqConfKeys.Srv, @@ -95,6 +96,10 @@ public static class EffectiveConfigSections DnsmasqConfKeys.Selfmx, ]), new SectionDef(SectionDhcp, "DHCP", [ + DnsmasqConfKeys.NoDhcpInterface, + DnsmasqConfKeys.NoDhcpv4Interface, + DnsmasqConfKeys.NoDhcpv6Interface, + DnsmasqConfKeys.EnableRa, DnsmasqConfKeys.DhcpAuthoritative, DnsmasqConfKeys.DhcpRapidCommit, DnsmasqConfKeys.LeasefileRo, @@ -161,7 +166,6 @@ public static class EffectiveConfigSections DnsmasqConfKeys.DnssecNoTimecheck, DnsmasqConfKeys.DnssecDebug, DnsmasqConfKeys.ProxyDnssec, - DnsmasqConfKeys.AddCpeId, DnsmasqConfKeys.DnssecTimestamp, DnsmasqConfKeys.DnssecLimits, DnsmasqConfKeys.TrustAnchor, @@ -175,8 +179,6 @@ public static class EffectiveConfigSections DnsmasqConfKeys.MaxTtl, DnsmasqConfKeys.MaxCacheTtl, DnsmasqConfKeys.MinCacheTtl, - DnsmasqConfKeys.Dumpfile, - DnsmasqConfKeys.Dumpmask, DnsmasqConfKeys.UseStaleCache, DnsmasqConfKeys.AddMac, DnsmasqConfKeys.StripMac, @@ -195,16 +197,14 @@ public static class EffectiveConfigSections DnsmasqConfKeys.ListenAddress, DnsmasqConfKeys.ExceptInterface, DnsmasqConfKeys.AuthServer, - DnsmasqConfKeys.NoDhcpInterface, - DnsmasqConfKeys.NoDhcpv4Interface, - DnsmasqConfKeys.NoDhcpv6Interface, + DnsmasqConfKeys.Dumpfile, + DnsmasqConfKeys.Dumpmask, DnsmasqConfKeys.PidFile, DnsmasqConfKeys.User, DnsmasqConfKeys.Group, DnsmasqConfKeys.LogFacility, DnsmasqConfKeys.EnableDbus, DnsmasqConfKeys.EnableUbus, - DnsmasqConfKeys.EnableRa, DnsmasqConfKeys.LogDhcp, DnsmasqConfKeys.KeepInForeground, DnsmasqConfKeys.NoDaemon, diff --git a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/EffectiveConfigViews.cs b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/EffectiveConfigViews.cs index 3ab70ee..edb8bf7 100644 --- a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/EffectiveConfigViews.cs +++ b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Metadata/EffectiveConfigViews.cs @@ -38,9 +38,8 @@ public static class EffectiveConfigViews DnsmasqConfKeys.NoHosts, DnsmasqConfKeys.AddnHosts, DnsmasqConfKeys.Hostsdir, - ]), - new(EffectiveConfigSections.SectionResolver, "Resolver / DNS", [ DnsmasqConfKeys.ExpandHosts, + DnsmasqConfKeys.Domain, ]), ]; diff --git a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Rendering/EffectiveConfigRenderFragmentRegistry.cs b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Rendering/EffectiveConfigRenderFragmentRegistry.cs index 0e25f34..2df6ff7 100644 --- a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Rendering/EffectiveConfigRenderFragmentRegistry.cs +++ b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Rendering/EffectiveConfigRenderFragmentRegistry.cs @@ -71,10 +71,9 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm private void RegisterFlagDisplays() { - RegisterFlags(EffectiveConfigSections.SectionHosts, DnsmasqConfKeys.NoHosts); + RegisterFlags(EffectiveConfigSections.SectionHosts, DnsmasqConfKeys.NoHosts, DnsmasqConfKeys.ExpandHosts); RegisterFlags( EffectiveConfigSections.SectionResolver, - DnsmasqConfKeys.ExpandHosts, DnsmasqConfKeys.BogusPriv, DnsmasqConfKeys.StrictOrder, DnsmasqConfKeys.AllServers, @@ -91,6 +90,7 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm DnsmasqConfKeys.NoRoundRobin); RegisterFlags( EffectiveConfigSections.SectionDhcp, + DnsmasqConfKeys.EnableRa, DnsmasqConfKeys.DhcpAuthoritative, DnsmasqConfKeys.ReadEthers, DnsmasqConfKeys.DhcpRapidCommit, @@ -125,7 +125,6 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm DnsmasqConfKeys.BindInterfaces, DnsmasqConfKeys.BindDynamic, DnsmasqConfKeys.LogDebug, - DnsmasqConfKeys.EnableRa, DnsmasqConfKeys.LogDhcp, DnsmasqConfKeys.KeepInForeground, DnsmasqConfKeys.NoDaemon, @@ -177,11 +176,9 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm RegisterSemanticSingles( EffectiveConfigSections.SectionDnssec, DnsmasqConfKeys.DnssecTimestamp); - RegisterSemanticSingles( - EffectiveConfigSections.SectionCache, - DnsmasqConfKeys.Dumpfile); RegisterSemanticSingles( EffectiveConfigSections.SectionProcess, + DnsmasqConfKeys.Dumpfile, DnsmasqConfKeys.PidFile); } @@ -193,7 +190,10 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm behavior: new DistinctMultiValueEditBehavior()); RegisterSemanticMultiDescriptor(EffectiveConfigSections.SectionProcess, DnsmasqConfKeys.ListenAddress); - RegisterSemanticMultis(EffectiveConfigSections.SectionHosts, DnsmasqConfKeys.AddnHosts); + RegisterSemanticMultis( + EffectiveConfigSections.SectionHosts, + DnsmasqConfKeys.AddnHosts, + DnsmasqConfKeys.Domain); RegisterSemanticMultis( EffectiveConfigSections.SectionResolver, DnsmasqConfKeys.Local, @@ -209,6 +209,9 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm DnsmasqConfKeys.ConnmarkAllowlist); RegisterSemanticMultis( EffectiveConfigSections.SectionDhcp, + DnsmasqConfKeys.NoDhcpInterface, + DnsmasqConfKeys.NoDhcpv4Interface, + DnsmasqConfKeys.NoDhcpv6Interface, DnsmasqConfKeys.DhcpRange, DnsmasqConfKeys.DhcpHost, DnsmasqConfKeys.DhcpOption, @@ -239,7 +242,6 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm DnsmasqConfKeys.FilterRr); RegisterSemanticMultis( EffectiveConfigSections.SectionDnsRecords, - DnsmasqConfKeys.Domain, DnsmasqConfKeys.Cname, DnsmasqConfKeys.MxHost, DnsmasqConfKeys.PtrRecord, @@ -273,10 +275,7 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm EffectiveConfigSections.SectionProcess, DnsmasqConfKeys.Interface, DnsmasqConfKeys.ExceptInterface, - DnsmasqConfKeys.AuthServer, - DnsmasqConfKeys.NoDhcpInterface, - DnsmasqConfKeys.NoDhcpv4Interface, - DnsmasqConfKeys.NoDhcpv6Interface); + DnsmasqConfKeys.AuthServer); } private void RegisterComponents(string sectionId, Type componentType, params string[] optionNames) diff --git a/src/DnsmasqWebUI/Models/Dnsmasq/PageStates/HostsPageState.cs b/src/DnsmasqWebUI/Models/Dnsmasq/PageStates/HostsPageState.cs index aebbc1c..ed999a9 100644 --- a/src/DnsmasqWebUI/Models/Dnsmasq/PageStates/HostsPageState.cs +++ b/src/DnsmasqWebUI/Models/Dnsmasq/PageStates/HostsPageState.cs @@ -39,11 +39,11 @@ public sealed record HostsPageState( : "Hosts editing is unavailable: managed hosts path is not configured. Set Dnsmasq:MainConfigPath (and optionally Dnsmasq:ManagedHostsFileName) so the app can create and edit the managed hosts file.", SystemHostsActive: systemHostsActive, SystemHostsInactiveReason: noHosts - ? "System hosts is ignored because no-hosts is enabled in dnsmasq config." + ? "Dnsmasq is ignoring system hosts (no-hosts is enabled), so system hosts rows are hidden from this table." : string.IsNullOrEmpty(status.SystemHostsPath) - ? "System hosts path is not configured." + ? "System hosts path is not configured; system hosts rows are hidden from this table." : !status.SystemHostsPathExists - ? $"System hosts file not found: {status.SystemHostsPath}" + ? $"System hosts file not found ({status.SystemHostsPath}); those rows are hidden from this table." : null, ExpandHostsEnabled: expandHosts, ExpansionDomain: expandHosts ? domain : null); diff --git a/src/DnsmasqWebUI/wwwroot/js/modules/table-scroll-cue.js b/src/DnsmasqWebUI/wwwroot/js/modules/table-scroll-cue.js index 5b76e36..4f20c98 100644 --- a/src/DnsmasqWebUI/wwwroot/js/modules/table-scroll-cue.js +++ b/src/DnsmasqWebUI/wwwroot/js/modules/table-scroll-cue.js @@ -158,3 +158,15 @@ export function disposeTableScrollCue() { if (right) right.remove(); }); } + +/** + * Returns true if the active element is not inside `container` (e.g. focus left a table row). + * Use after a short delay following focusout so document.activeElement has updated. + * @param {HTMLElement | null} container + */ +export function isFocusOutsideElement(container) { + if (!container) return true; + const active = document.activeElement; + if (!active) return true; + return !container.contains(active); +}