Prepare release 0.0.1: install script (uninstall/purge, service, validation), README (how to use, config), UI and effective config updates

This commit is contained in:
2026-02-05 00:05:52 +10:00
parent 8c411b8d09
commit 4ff9c552eb
28 changed files with 633 additions and 46 deletions
+6 -2
View File
@@ -11,8 +11,12 @@
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["DnsmasqWebUI.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" sizes="32x32" />
<link rel="apple-touch-icon" href="logo.png" />
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32.png" />
<link rel="icon" type="image/png" sizes="48x48" href="favicon-48.png" />
<link rel="icon" type="image/png" sizes="192x192" href="favicon-192.png" />
<link rel="icon" type="image/png" sizes="512x512" href="favicon-512.png" />
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
<title>@_appTitle</title>
<HeadOutlet />
</head>
@@ -0,0 +1,24 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig.CustomDisplays
@using DnsmasqWebUI.Models.EffectiveConfig
@* Reusable value-only display for boolean/flag options. Edit mode: checkbox (unless readonly); View: "Enabled" / "Disabled". *@
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && Descriptor.GetSource()?.IsReadOnly != true)
{
var isSet = Descriptor.GetValue() is bool b && b;
<input type="checkbox" class="form-check-input" checked="@isSet" @onchange="OnCheckedChange" />
}
else
{
@(Descriptor.GetValue() is bool isSet && isSet ? "Enabled" : "Disabled")
}
@code {
[Parameter] public EffectiveConfigFieldDescriptor Descriptor { get; set; } = null!;
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
[Parameter] public EventCallback<bool> CheckedChanged { get; set; }
private async Task OnCheckedChange(ChangeEventArgs e)
{
await CheckedChanged.InvokeAsync(e.Value is true);
}
}
@@ -0,0 +1,27 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig.CustomDisplays
@using DnsmasqWebUI.Models.EffectiveConfig
@* Value-only display for the port option. Edit mode: number input (unless readonly); View: text with "(default DNS port)". *@
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && Descriptor.GetSource()?.IsReadOnly != true)
{
var port = Descriptor.GetValue() is int p ? p : 53;
<input type="number" class="form-control form-control-sm" style="max-width: 6rem;" min="1" max="65535" value="@port" @oninput="OnPortInput" />
}
else
{
@(Descriptor.GetValue() is int port ? $"{port} (default DNS port)" : "(not set)")
}
@code {
[Parameter] public EffectiveConfigFieldDescriptor Descriptor { get; set; } = null!;
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
[Parameter] public EventCallback<int?> PortChanged { get; set; }
private async Task OnPortInput(ChangeEventArgs e)
{
if (int.TryParse(e.Value?.ToString(), out var port) && port >= 1 && port <= 65535)
await PortChanged.InvokeAsync(port);
else
await PortChanged.InvokeAsync(null);
}
}
@@ -1,18 +1,44 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.EffectiveConfig
@using DnsmasqWebUI.Models.Config
@using DnsmasqWebUI.Services.Abstractions
@inject IEffectiveConfigRenderFragmentRegistry RenderFragmentRegistry
@if (Descriptor.IsMultiValue)
@* Custom fragment renders only the value; we keep the row shell (label + value slot + badge) here for consistency. *@
@if (_customFragment is { } fragment)
{
var labelTooltip = DnsmasqOptionTooltips.Get(Descriptor.OptionName);
<li class="ec-field" title="@(Descriptor.GetSource()?.GetReadOnlyTooltip())">
<span class="ec-field-label" title="@(labelTooltip ?? Descriptor.GetSource()?.GetReadOnlyTooltip())"><strong>@Descriptor.OptionName:</strong></span>
<span class="ec-field-value">@fragment(Descriptor)</span>
@if (Descriptor.GetSource()?.IsReadOnly == true)
{
<span class="badge bg-secondary ms-1">readonly</span>
}
</li>
}
else if (Descriptor.IsMultiValue)
{
var items = Descriptor.GetItems();
<EffectiveConfigMultiValueRow Label="@Descriptor.OptionName" Items="@items" />
<EffectiveConfigMultiValueRow Label="@Descriptor.OptionName" Items="@items" LabelTooltip="@_labelTooltip" />
}
else
{
var value = Descriptor.GetValue();
var displayValue = value == null ? "(not set)" : (value is IReadOnlyList<string> list ? (list.Count == 0 ? "(none)" : string.Join(", ", list)) : value.ToString());
<EffectiveConfigOptionItem Label="@Descriptor.OptionName" DisplayValue="@(displayValue ?? "(not set)")" Source="@Descriptor.GetSource()" />
<EffectiveConfigOptionItem Label="@Descriptor.OptionName" DisplayValue="@(displayValue ?? "(not set)")" Source="@Descriptor.GetSource()" LabelTooltip="@_labelTooltip" />
}
@code {
[Parameter] public EffectiveConfigFieldDescriptor Descriptor { get; set; } = null!;
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
private RenderFragment<EffectiveConfigFieldDescriptor>? _customFragment;
private string? _labelTooltip;
protected override void OnParametersSet()
{
_customFragment = RenderFragmentRegistry.GetDisplayFragment(Descriptor.SectionId, Descriptor.OptionName);
_labelTooltip = DnsmasqOptionTooltips.Get(Descriptor.OptionName);
}
}
@@ -1,7 +1,7 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.EffectiveConfig
<span class="d-inline-block me-2" title="@(Source?.GetReadOnlyTooltip())">
<span class="ec-field-value-item" title="@(Source?.GetReadOnlyTooltip())">
@Value
@if (Source?.IsReadOnly == true)
{
@@ -1,24 +1,56 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.EffectiveConfig
<li>
<strong>@Label:</strong>
@if (Items?.Count > 0)
<li class="ec-field">
<span class="ec-field-label" title="@LabelTooltip"><strong>@Label:</strong></span>
@if (DisplayMode == EffectiveConfigDisplayMode.Edit)
{
@for (var i = 0; i < Items.Count; i++)
{
if (i > 0) { <span class="me-2">, </span> }
var item = Items[i];
<EffectiveConfigMultiValueItem Value="@item.Value" Source="@item.Source" />
}
<span class="ec-field-value ec-field-value-multi">
<textarea class="form-control form-control-sm" rows="@(Math.Max(2, (Items?.Count ?? 0) + 1))" @oninput="OnLinesInput" placeholder="One value per line">@_editLines</textarea>
</span>
}
else
{
<span class="text-muted">(none)</span>
<span class="ec-field-value ec-field-value-multi">
@if (Items?.Count > 0)
{
<ol class="ec-field-value-list">
@foreach (var item in Items!)
{
<li><EffectiveConfigMultiValueItem Value="@item.Value" Source="@item.Source" /></li>
}
</ol>
}
else
{
<span class="text-muted">(none)</span>
}
</span>
}
</li>
@code {
[Parameter] public string Label { get; set; } = "";
[Parameter] public IReadOnlyList<ValueWithSource>? Items { get; set; }
[Parameter] public EventCallback<IReadOnlyList<string>> ItemsChanged { get; set; }
[Parameter] public string? LabelTooltip { get; set; }
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
private string _editLines = "";
protected override void OnParametersSet()
{
if (Items != null && Items.Count > 0)
_editLines = string.Join("\n", Items.Select(i => i.Value));
else
_editLines = "";
}
private async Task OnLinesInput(ChangeEventArgs e)
{
var text = e.Value?.ToString() ?? "";
_editLines = text;
var lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(l => l.Trim()).Where(l => l.Length > 0).ToList();
await ItemsChanged.InvokeAsync(lines);
}
}
@@ -1,10 +1,21 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.EffectiveConfig
<li title="@(Source?.GetReadOnlyTooltip())">
<strong>@Label:</strong> @DisplayValue
@if (Source?.IsReadOnly == true)
<li class="ec-field" title="@(Source?.GetReadOnlyTooltip())">
<span class="ec-field-label" title="@(LabelTooltip ?? Source?.GetReadOnlyTooltip())"><strong>@Label:</strong></span>
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && Source?.IsReadOnly != true)
{
<span class="badge bg-secondary ms-1">readonly</span>
<span class="ec-field-value">
<input type="text" class="form-control form-control-sm" value="@DisplayValue" @oninput="OnValueInput" />
</span>
}
else
{
<span class="ec-field-value">@DisplayValue</span>
@if (Source?.IsReadOnly == true)
{
<span class="badge bg-secondary ms-1">readonly</span>
}
}
</li>
@@ -12,4 +23,15 @@
[Parameter] public string Label { get; set; } = "";
[Parameter] public string DisplayValue { get; set; } = "";
[Parameter] public ConfigValueSource? Source { get; set; }
/// <summary>Optional explainer shown on hover over the label (e.g. from DnsmasqOptionTooltips).</summary>
[Parameter] public string? LabelTooltip { get; set; }
/// <summary>When Edit and not readonly: raised when the user changes the value. Not yet wired to save.</summary>
[Parameter] public EventCallback<string> DisplayValueChanged { get; set; }
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
private async Task OnValueInput(ChangeEventArgs e)
{
var value = e.Value?.ToString() ?? "";
await DisplayValueChanged.InvokeAsync(value);
}
}
@@ -21,7 +21,7 @@
var sectionDescriptors = descriptors.Where(d => d.SectionId == sectionId).ToList();
if (sectionDescriptors.Count > 0)
{
<EffectiveConfigSectionPanel SectionId="@sectionId" Title="@title" Descriptors="@sectionDescriptors" IsOpen="@IsOpen(sectionId)" OnToggle="@OnSectionToggle" />
<EffectiveConfigSectionPanel SectionId="@sectionId" Title="@title" Descriptors="@sectionDescriptors" IsOpen="@IsOpen(sectionId)" OnToggle="@OnSectionToggle" DisplayMode="@DisplayMode" />
}
}
</div>
@@ -29,6 +29,8 @@
@code {
[Parameter] public DnsmasqServiceStatus? Status { get; set; }
/// <summary>View = read-only; Edit = editable (future). Cascaded to field displays.</summary>
[Parameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
private HashSet<string> _openPanels = new();
@@ -14,14 +14,14 @@
border-bottom: none;
}
/* Touch-friendly header: min 44px tap target (WCAG 2.5.5) */
/* Touch-friendly header: min 44px tap target (WCAG 2.5.5), breathing room from edges */
::deep .ec-section-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: 2.75rem;
padding: 0.5rem 0;
padding: 0.625rem 1rem;
margin: 0;
border: none;
border-radius: 0.25rem;
@@ -75,9 +75,10 @@
}
::deep .ec-section-body {
padding: 0.25rem 0 0.5rem 0;
padding-left: 0.5rem;
padding: 0.25rem 1rem 0.5rem 1rem;
padding-left: 1rem;
border-left: 2px solid var(--bs-border-color-translucent, rgba(0, 0, 0, 0.075));
margin-left: 0;
overflow-wrap: break-word;
word-break: break-word;
}
@@ -93,8 +94,69 @@
word-break: break-word;
}
/* Field row: label + value + optional badge. Same structure for custom and default displays. */
::deep .ec-field {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.25rem 0.5rem;
margin-bottom: 0.35rem;
}
::deep .ec-field:last-child {
margin-bottom: 0;
}
::deep .ec-field-label {
flex-shrink: 0;
min-width: 0;
}
::deep .ec-field-value {
flex: 1 1 auto;
min-width: 0;
overflow-wrap: break-word;
word-break: break-word;
}
/* Multi-value: full width below label, indented ordered list */
::deep .ec-field-value-multi {
flex-basis: 100%;
padding-left: 1.25rem;
margin-top: 0.15rem;
}
::deep .ec-field-value-list {
margin: 0;
padding-left: 1.25rem;
list-style-type: decimal;
}
::deep .ec-field-value-list li {
margin-bottom: 0.2rem;
overflow-wrap: break-word;
word-break: break-word;
}
::deep .ec-field-value-list li:last-child {
margin-bottom: 0;
}
::deep .ec-field-value-item {
display: inline;
}
::deep .ec-field-value-sep {
margin-right: 0.25rem;
}
@media (max-width: 767.98px) {
::deep .ec-section-header {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
::deep .ec-section-body {
padding-left: 0.35rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
}
}
@@ -1,22 +1,24 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.EffectiveConfig
<div class="ec-section @(IsOpen ? "ec-section-open" : "")">
<button type="button" class="ec-section-header" @onclick="ToggleSection" aria-expanded="@IsOpen">
<span class="ec-section-title fw-bold">@Title</span>
<i class="bi @(IsOpen ? "bi-chevron-down" : "bi-chevron-right") ec-section-chevron" aria-hidden="true"></i>
</button>
<div class="collapse @(IsOpen ? "show" : "")">
<div class="ec-section-body">
<ul class="list-unstyled mb-0">
@foreach (var f in Descriptors)
{
<EffectiveConfigFieldDisplay Descriptor="@f" />
}
</ul>
<CascadingValue Value="DisplayMode">
<div class="ec-section @(IsOpen ? "ec-section-open" : "")">
<button type="button" class="ec-section-header" @onclick="ToggleSection" aria-expanded="@IsOpen">
<span class="ec-section-title fw-bold">@Title</span>
<i class="bi @(IsOpen ? "bi-chevron-down" : "bi-chevron-right") ec-section-chevron" aria-hidden="true"></i>
</button>
<div class="collapse @(IsOpen ? "show" : "")">
<div class="ec-section-body">
<ul class="list-unstyled mb-0">
@foreach (var f in Descriptors)
{
<EffectiveConfigFieldDisplay Descriptor="@f" />
}
</ul>
</div>
</div>
</div>
</div>
</CascadingValue>
@code {
[Parameter] public string SectionId { get; set; } = "";
@@ -24,6 +26,8 @@
[Parameter] public IReadOnlyList<EffectiveConfigFieldDescriptor> Descriptors { get; set; } = null!;
[Parameter] public bool IsOpen { get; set; }
[Parameter] public EventCallback<string> OnToggle { get; set; }
/// <summary>Cascaded from EffectiveConfigSection. View = read-only; Edit = editable (future).</summary>
[Parameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
private async Task ToggleSection() => await OnToggle.InvokeAsync(SectionId);
}
@@ -4,7 +4,7 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid d-flex align-items-center">
<a class="navbar-brand d-flex align-items-center gap-2" href="" aria-label="@AppTitle home">
<img src="logo.png" alt="" class="brand-logo" />
<img src="logo.webp" alt="" class="brand-logo" />
<span>@AppTitle</span>
</a>
</div>
@@ -0,0 +1,67 @@
using System.Collections.Frozen;
namespace DnsmasqWebUI.Models.Config;
/// <summary>
/// Short explainer tooltips for dnsmasq option names. Shown on hover over the field label.
/// One entry per effective-config key for consistency.
/// </summary>
public static class DnsmasqOptionTooltips
{
/// <summary>Display label used in EffectiveConfigFieldBuilder for server/local multi-value.</summary>
public const string ServerLocalLabel = "server / local";
private static readonly FrozenDictionary<string, string> Tooltips = new Dictionary<string, string>(StringComparer.Ordinal)
{
// --- Hosts ---
[DnsmasqConfKeys.NoHosts] = "Do not read /etc/hosts; only use addn-hosts and DHCP/hosts.",
[DnsmasqConfKeys.AddnHosts] = "Additional hosts files (one per line).",
// --- Resolver / DNS ---
[DnsmasqConfKeys.ExpandHosts] = "Expand simple names in /etc/hosts with the local domain.",
[DnsmasqConfKeys.BogusPriv] = "Do not forward reverse lookups for private IP ranges.",
[DnsmasqConfKeys.StrictOrder] = "Query servers in the order specified; do not try the next on failure.",
[DnsmasqConfKeys.NoResolv] = "Do not read /etc/resolv.conf; use only server= and resolv-file=.",
[DnsmasqConfKeys.DomainNeeded] = "Never forward plain names (without a domain) to upstream.",
[DnsmasqConfKeys.Port] = "Listen on this port for DNS queries (default 53).",
[ServerLocalLabel] = "Upstream DNS servers (server=) and local-only domains (local=). Order is preserved.",
[DnsmasqConfKeys.Address] = "Map a domain or hostname to an IP (e.g. for ad blocking or local names). Can repeat.",
[DnsmasqConfKeys.ResolvFile] = "File(s) to read upstream server addresses from (e.g. from DHCP). Can repeat.",
// --- DHCP ---
[DnsmasqConfKeys.DhcpAuthoritative] = "DHCP server is authoritative; required for DHCP to work.",
[DnsmasqConfKeys.LeasefileRo] = "Do not create or truncate the DHCP lease file.",
[DnsmasqConfKeys.DhcpLeasefile] = "Path to the DHCP lease file.",
[DnsmasqConfKeys.DhcpLeaseMax] = "Maximum number of DHCP leases to hold.",
[DnsmasqConfKeys.DhcpTtl] = "TTL for DHCP names in the DNS cache.",
[DnsmasqConfKeys.DhcpRange] = "DHCP address range(s) and lease time. Can repeat.",
[DnsmasqConfKeys.DhcpHost] = "DHCP host entries (reservations by MAC or client-id). Can repeat.",
[DnsmasqConfKeys.DhcpOption] = "DHCP option lines sent to clients. Can repeat.",
// --- Cache ---
[DnsmasqConfKeys.CacheSize] = "Maximum number of DNS cache entries (0 to disable).",
[DnsmasqConfKeys.LocalTtl] = "TTL for entries from /etc/hosts and DHCP.",
[DnsmasqConfKeys.NoNegcache] = "Do not cache negative (NXDOMAIN) responses.",
[DnsmasqConfKeys.NegTtl] = "TTL for negative (cached miss) responses.",
[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.",
// --- Process & networking ---
[DnsmasqConfKeys.NoPoll] = "Do not poll /etc/resolv.conf for changes.",
[DnsmasqConfKeys.BindInterfaces] = "Only bind to interfaces in use; do not bind to wildcard.",
[DnsmasqConfKeys.Interface] = "Only use these interfaces for DHCP/DNS. Can repeat.",
[DnsmasqConfKeys.ListenAddress] = "Listen on these addresses for DNS/DHCP. Can repeat.",
[DnsmasqConfKeys.ExceptInterface] = "Do not use these interfaces. Can repeat.",
[DnsmasqConfKeys.PidFile] = "Write process ID to this file.",
[DnsmasqConfKeys.User] = "Run as this user after startup (requires root).",
[DnsmasqConfKeys.Group] = "Run as this group after startup (requires root).",
[DnsmasqConfKeys.LogFacility] = "Syslog facility for dnsmasq logs (e.g. local0, daemon).",
}.ToFrozenDictionary();
/// <summary>Returns a short tooltip for the option, or null if none is defined.</summary>
public static string? Get(string optionName)
{
return Tooltips.TryGetValue(optionName, out var tip) ? tip : null;
}
}
@@ -0,0 +1,14 @@
namespace DnsmasqWebUI.Models.EffectiveConfig;
/// <summary>
/// Display mode for effective-config fields. Cascaded from the parent so that
/// OptionItem, MultiValueRow, and custom value displays can render view vs edit appropriately.
/// </summary>
public enum EffectiveConfigDisplayMode
{
/// <summary>Show values as read-only text.</summary>
View,
/// <summary>Show editable controls (inputs, toggles). Not yet implemented; plumbing only.</summary>
Edit,
}
@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Components;
using DnsmasqWebUI.Models.EffectiveConfig;
namespace DnsmasqWebUI.Services.Abstractions;
/// <summary>
/// Lookup for custom display fragments per effective-config field (section + option).
/// When non-null, the fragment renders the value portion of the row; label and source badge stay shared.
/// Registered as singleton via assembly scanning (<see cref="IApplicationSingleton"/>).
/// </summary>
public interface IEffectiveConfigRenderFragmentRegistry : IApplicationSingleton
{
/// <summary>
/// Returns a fragment that renders the custom value UI for this field, or null to use default rendering.
/// </summary>
RenderFragment<EffectiveConfigFieldDescriptor>? GetDisplayFragment(string sectionId, string optionName);
}
@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Components;
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig.CustomDisplays;
namespace DnsmasqWebUI.Services;
/// <summary>
/// Maps (sectionId, optionName) to custom effective-config value display component types and builds
/// RenderFragments that render those components with the descriptor.
/// </summary>
public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragmentRegistry
{
private readonly Dictionary<(string SectionId, string OptionName), Type> _displayComponents = new();
public EffectiveConfigRenderFragmentRegistry()
{
// Port: single-field display (e.g. "53 (default DNS port)").
_displayComponents[(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.Port)] = typeof(PortValueDisplay);
// Boolean flags: shared display ("Enabled" / "Disabled") for all flag options.
RegisterFlag(EffectiveConfigFieldBuilder.SectionHosts, DnsmasqConfKeys.NoHosts);
RegisterFlag(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.ExpandHosts);
RegisterFlag(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.BogusPriv);
RegisterFlag(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.StrictOrder);
RegisterFlag(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.NoResolv);
RegisterFlag(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.DomainNeeded);
RegisterFlag(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.NoNegcache);
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.NoPoll);
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.BindInterfaces);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpAuthoritative);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.LeasefileRo);
}
private void RegisterFlag(string sectionId, string optionName)
{
_displayComponents[(sectionId, optionName)] = typeof(FlagValueDisplay);
}
/// <inheritdoc />
public RenderFragment<EffectiveConfigFieldDescriptor>? GetDisplayFragment(string sectionId, string optionName)
{
if (!_displayComponents.TryGetValue((sectionId, optionName), out var componentType))
return null;
return descriptor => builder =>
{
builder.OpenComponent(0, componentType);
builder.AddAttribute(1, "Descriptor", descriptor);
builder.CloseComponent();
};
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB