Prepare release 0.0.1: install script (uninstall/purge, service, validation), README (how to use, config), UI and effective config updates
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 761 B |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 5.4 MiB |
|
After Width: | Height: | Size: 85 KiB |