Add DNS Records and Filters pages; unify effective config layout

- Add EffectiveConfigContext.DnsRecords and .Filters; map in EffectiveConfigViews
- Add DnsRecords.razor and Filters.razor with context-scoped effective config
- Add nav links for DNS records and Filters (icons: diagram-3-fill, funnel-fill)
- Put effective config block at top of Hosts, DHCP, DNS Records, Filters (no config file list on sub-pages)
- Hosts view: add expand-hosts to allow-list. Filters view: add domain-needed, bogus-priv, filterwin2k, no-round-robin
- Page titles: Hosts Configuration, DHCP Configuration, DNS Records Configuration, Filters Configuration
- Remove 'Config affecting...' subheadings and page intro text; add .page-intro and .page-chip styles
This commit is contained in:
2026-03-09 23:22:13 +10:00
parent de8afed68c
commit fd16d50107
8 changed files with 219 additions and 7 deletions
@@ -39,6 +39,16 @@
<i class="bi bi-hdd-network nav-icon" aria-hidden="true"></i> DHCP <i class="bi bi-hdd-network nav-icon" aria-hidden="true"></i> DHCP
</NavLink> </NavLink>
</div> </div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="dns-records">
<i class="bi bi-diagram-3-fill nav-icon" aria-hidden="true"></i> DNS records
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="filters">
<i class="bi bi-funnel-fill nav-icon" aria-hidden="true"></i> Filters
</NavLink>
</div>
</nav> </nav>
<div class="nav-footer px-3 py-2" aria-label="Links and version"> <div class="nav-footer px-3 py-2" aria-label="Links and version">
<a class="nav-footer-link" href="https://github.com/alexhopeoconnor/dnsmasq-webui" target="_blank" rel="noopener noreferrer" title="dnsmasq-webui project on GitHub">Project</a> <a class="nav-footer-link" href="https://github.com/alexhopeoconnor/dnsmasq-webui" target="_blank" rel="noopener noreferrer" title="dnsmasq-webui project on GitHub">Project</a>
+5 -5
View File
@@ -13,7 +13,7 @@
<PageTitle>DHCP | @AppTitle</PageTitle> <PageTitle>DHCP | @AppTitle</PageTitle>
<h1 class="page-title">DHCP</h1> <h1 class="page-title">DHCP Configuration</h1>
@if (_status == null) @if (_status == null)
{ {
@@ -24,6 +24,10 @@
} }
else else
{ {
<section class="page-section">
<EffectiveConfigSection Status="_status" Context="EffectiveConfigContext.Dhcp" ShowSearchBox="true" OnSaveCompleted="RefreshStatusAsync" />
</section>
@* ---- Static hosts (dhcp-host= in managed file) ---- *@ @* ---- Static hosts (dhcp-host= in managed file) ---- *@
<section class="page-section"> <section class="page-section">
<h2 class="page-section-title">Static hosts</h2> <h2 class="page-section-title">Static hosts</h2>
@@ -323,10 +327,6 @@ else
} }
</section> </section>
<section class="page-section">
<h2 class="page-section-title">Config affecting DHCP</h2>
<EffectiveConfigSection Status="_status" Context="EffectiveConfigContext.Dhcp" ShowSearchBox="true" OnSaveCompleted="RefreshStatusAsync" />
</section>
} }
@code { @code {
@@ -0,0 +1,64 @@
@page "/dns-records"
@rendermode InteractiveServer
@namespace DnsmasqWebUI.Components.Pages
@inject IStatusClient StatusClient
@inject IOptions<ApplicationOptions> AppOptions
<PageTitle>DNS records | @AppTitle</PageTitle>
<h1 class="page-title">DNS Records Configuration</h1>
@if (_status == null)
{
<p class="d-flex align-items-center gap-2">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Loading...
</p>
}
else
{
<section class="page-section">
<EffectiveConfigSection Status="_status" Context="EffectiveConfigContext.DnsRecords" ShowSearchBox="true" OnSaveCompleted="RefreshStatusAsync" />
</section>
<div class="page-chips" role="group" aria-label="Record type">
<span class="page-chip active">All</span>
<span class="page-chip">CNAME</span>
<span class="page-chip">MX / SRV</span>
<span class="page-chip">PTR</span>
<span class="page-chip">TXT</span>
<span class="page-chip">A / AAAA</span>
<span class="page-chip">Other</span>
</div>
}
@code {
private string AppTitle => AppOptions!.Value!.EffectiveTitle;
private DnsmasqServiceStatus? _status;
protected override async Task OnInitializedAsync()
{
await Load();
}
private async Task RefreshStatusAsync()
{
try
{
_status = await StatusClient.GetStatusAsync();
await InvokeAsync(StateHasChanged);
}
catch { /* ignore */ }
}
private async Task Load()
{
try
{
_status = await StatusClient.GetStatusAsync();
}
catch
{
// Status will stay null; UI shows loading or empty
}
}
}
@@ -0,0 +1,63 @@
@page "/filters"
@rendermode InteractiveServer
@namespace DnsmasqWebUI.Components.Pages
@inject IStatusClient StatusClient
@inject IOptions<ApplicationOptions> AppOptions
<PageTitle>Filters | @AppTitle</PageTitle>
<h1 class="page-title">Filters Configuration</h1>
@if (_status == null)
{
<p class="d-flex align-items-center gap-2">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Loading...
</p>
}
else
{
<section class="page-section">
<EffectiveConfigSection Status="_status" Context="EffectiveConfigContext.Filters" ShowSearchBox="true" OnSaveCompleted="RefreshStatusAsync" />
</section>
<div class="page-chips" role="group" aria-label="Filter policy category">
<span class="page-chip active">All</span>
<span class="page-chip">Blocking / sinkhole</span>
<span class="page-chip">Split DNS</span>
<span class="page-chip">Safety</span>
<span class="page-chip">Response shaping</span>
<span class="page-chip">Firewall</span>
</div>
}
@code {
private string AppTitle => AppOptions!.Value!.EffectiveTitle;
private DnsmasqServiceStatus? _status;
protected override async Task OnInitializedAsync()
{
await Load();
}
private async Task RefreshStatusAsync()
{
try
{
_status = await StatusClient.GetStatusAsync();
await InvokeAsync(StateHasChanged);
}
catch { /* ignore */ }
}
private async Task Load()
{
try
{
_status = await StatusClient.GetStatusAsync();
}
catch
{
// Status will stay null; UI shows loading or empty
}
}
}
@@ -6,7 +6,7 @@
@inject IOptions<ApplicationOptions> AppOptions @inject IOptions<ApplicationOptions> AppOptions
<PageTitle>Hosts | @AppTitle</PageTitle> <PageTitle>Hosts | @AppTitle</PageTitle>
<h1 class="page-title">Hosts file</h1> <h1 class="page-title">Hosts Configuration</h1>
@if (_status == null) @if (_status == null)
{ {
@@ -18,7 +18,6 @@
else else
{ {
<section class="page-section mb-4"> <section class="page-section mb-4">
<h2 class="page-section-title">Config affecting hosts</h2>
<EffectiveConfigSection Status="_status" Context="EffectiveConfigContext.Hosts" ShowSearchBox="true" OnSaveCompleted="RefreshStatusAsync" /> <EffectiveConfigSection Status="_status" Context="EffectiveConfigContext.Hosts" ShowSearchBox="true" OnSaveCompleted="RefreshStatusAsync" />
</section> </section>
@if (string.IsNullOrEmpty(_status.ManagedHostsFilePath)) @if (string.IsNullOrEmpty(_status.ManagedHostsFilePath))
@@ -20,6 +20,8 @@ public static class EffectiveConfigViews
EffectiveConfigContext.All => GetAllSections(), EffectiveConfigContext.All => GetAllSections(),
EffectiveConfigContext.Hosts => GetHostsView(), EffectiveConfigContext.Hosts => GetHostsView(),
EffectiveConfigContext.Dhcp => GetDhcpView(), EffectiveConfigContext.Dhcp => GetDhcpView(),
EffectiveConfigContext.DnsRecords => GetDnsRecordsView(),
EffectiveConfigContext.Filters => GetFiltersView(),
_ => [], _ => [],
}; };
} }
@@ -38,6 +40,7 @@ public static class EffectiveConfigViews
DnsmasqConfKeys.AddnHosts, DnsmasqConfKeys.AddnHosts,
DnsmasqConfKeys.Hostsdir, DnsmasqConfKeys.Hostsdir,
DnsmasqConfKeys.ReadEthers, DnsmasqConfKeys.ReadEthers,
DnsmasqConfKeys.ExpandHosts,
]), ]),
]; ];
@@ -47,6 +50,38 @@ public static class EffectiveConfigViews
new(EffectiveConfigSections.SectionTftpPxe, "TFTP / PXE", null), new(EffectiveConfigSections.SectionTftpPxe, "TFTP / PXE", null),
]; ];
private static IReadOnlyList<EffectiveConfigSectionView> GetDnsRecordsView() =>
[
new(EffectiveConfigSections.SectionDnsRecords, "DNS records", null),
];
private static IReadOnlyList<EffectiveConfigSectionView> GetFiltersView() =>
[
new(EffectiveConfigSections.SectionResolver, "Resolver / DNS", [
DnsmasqConfKeys.DomainNeeded,
DnsmasqConfKeys.Local,
DnsmasqConfKeys.Server,
DnsmasqConfKeys.RevServer,
DnsmasqConfKeys.Address,
DnsmasqConfKeys.BogusPriv,
DnsmasqConfKeys.BogusNxdomain,
DnsmasqConfKeys.IgnoreAddress,
DnsmasqConfKeys.Alias,
DnsmasqConfKeys.Filterwin2k,
DnsmasqConfKeys.FilterA,
DnsmasqConfKeys.FilterAaaa,
DnsmasqConfKeys.FilterRr,
DnsmasqConfKeys.StopDnsRebind,
DnsmasqConfKeys.RebindLocalhostOk,
DnsmasqConfKeys.RebindDomainOk,
DnsmasqConfKeys.Ipset,
DnsmasqConfKeys.Nftset,
DnsmasqConfKeys.ConnmarkAllowlistEnable,
DnsmasqConfKeys.ConnmarkAllowlist,
DnsmasqConfKeys.NoRoundRobin,
]),
];
/// <summary> /// <summary>
/// For each section in the view, returns the context-visible descriptors for that section. /// For each section in the view, returns the context-visible descriptors for that section.
/// Does not apply search; component applies search to each section's list and hides sections with no matches. /// Does not apply search; component applies search to each section's list and hides sections with no matches.
@@ -13,4 +13,10 @@ public enum EffectiveConfigContext
/// <summary>DHCP page: DHCP and TFTP/PXE sections.</summary> /// <summary>DHCP page: DHCP and TFTP/PXE sections.</summary>
Dhcp, Dhcp,
/// <summary>DNS records page: only DNS records section.</summary>
DnsRecords,
/// <summary>Filters page: resolver options for blocking, split DNS, safety, ipset/nftset.</summary>
Filters,
} }
+35
View File
@@ -165,6 +165,41 @@ h1:focus {
margin-bottom: var(--app-spacing-sm); margin-bottom: var(--app-spacing-sm);
} }
/* Page intro – short description under the h1 */
.page-intro {
color: var(--bs-secondary);
font-size: 0.9375rem;
margin-bottom: var(--app-spacing-md);
max-width: var(--app-content-max-width);
}
/* Category/record-type chips – visual grouping, optional filter later */
.page-chips {
display: flex;
flex-wrap: wrap;
gap: var(--app-spacing-xs);
margin-bottom: var(--app-spacing-md);
}
.page-chip {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.5rem;
font-size: 0.8125rem;
font-weight: 500;
border-radius: 9999px;
border: 1px solid var(--bs-border-color);
background: var(--bs-body-bg);
color: var(--bs-body-color);
cursor: default;
}
.page-chip.active {
border-color: var(--app-link-color);
background: rgba(13, 110, 253, 0.08);
color: var(--app-link-color);
}
/* Wrap text instead of horizontal scroll (WCAG-friendly, better on mobile). pre-wrap keeps newlines so line-by-line is clear. */ /* Wrap text instead of horizontal scroll (WCAG-friendly, better on mobile). pre-wrap keeps newlines so line-by-line is clear. */
.pre-block { .pre-block {
font-family: var(--app-font-mono); font-family: var(--app-font-mono);