mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-03 22:12:00 +10:00
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:
@@ -39,6 +39,16 @@
|
||||
<i class="bi bi-hdd-network nav-icon" aria-hidden="true"></i> DHCP
|
||||
</NavLink>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<PageTitle>DHCP | @AppTitle</PageTitle>
|
||||
|
||||
<h1 class="page-title">DHCP</h1>
|
||||
<h1 class="page-title">DHCP Configuration</h1>
|
||||
|
||||
@if (_status == null)
|
||||
{
|
||||
@@ -24,6 +24,10 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<section class="page-section">
|
||||
<EffectiveConfigSection Status="_status" Context="EffectiveConfigContext.Dhcp" ShowSearchBox="true" OnSaveCompleted="RefreshStatusAsync" />
|
||||
</section>
|
||||
|
||||
@* ---- Static hosts (dhcp-host= in managed file) ---- *@
|
||||
<section class="page-section">
|
||||
<h2 class="page-section-title">Static hosts</h2>
|
||||
@@ -323,10 +327,6 @@ else
|
||||
}
|
||||
</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 {
|
||||
|
||||
@@ -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
|
||||
<PageTitle>Hosts | @AppTitle</PageTitle>
|
||||
|
||||
<h1 class="page-title">Hosts file</h1>
|
||||
<h1 class="page-title">Hosts Configuration</h1>
|
||||
|
||||
@if (_status == null)
|
||||
{
|
||||
@@ -18,7 +18,6 @@
|
||||
else
|
||||
{
|
||||
<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" />
|
||||
</section>
|
||||
@if (string.IsNullOrEmpty(_status.ManagedHostsFilePath))
|
||||
|
||||
@@ -20,6 +20,8 @@ public static class EffectiveConfigViews
|
||||
EffectiveConfigContext.All => GetAllSections(),
|
||||
EffectiveConfigContext.Hosts => GetHostsView(),
|
||||
EffectiveConfigContext.Dhcp => GetDhcpView(),
|
||||
EffectiveConfigContext.DnsRecords => GetDnsRecordsView(),
|
||||
EffectiveConfigContext.Filters => GetFiltersView(),
|
||||
_ => [],
|
||||
};
|
||||
}
|
||||
@@ -38,6 +40,7 @@ public static class EffectiveConfigViews
|
||||
DnsmasqConfKeys.AddnHosts,
|
||||
DnsmasqConfKeys.Hostsdir,
|
||||
DnsmasqConfKeys.ReadEthers,
|
||||
DnsmasqConfKeys.ExpandHosts,
|
||||
]),
|
||||
];
|
||||
|
||||
@@ -47,6 +50,38 @@ public static class EffectiveConfigViews
|
||||
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>
|
||||
/// 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.
|
||||
|
||||
@@ -13,4 +13,10 @@ public enum EffectiveConfigContext
|
||||
|
||||
/// <summary>DHCP page: DHCP and TFTP/PXE sections.</summary>
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -165,6 +165,41 @@ h1:focus {
|
||||
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. */
|
||||
.pre-block {
|
||||
font-family: var(--app-font-mono);
|
||||
|
||||
Reference in New Issue
Block a user