Files
dnsmasq-webui/src/DnsmasqWebUI/Components/Pages/Dnsmasq.razor
T
alex a03e93bcd3 Effective config: do-0x20-encode revert fix, validation UI, custom displays, option help
- Fix do-0x20-encode: clearing pending when toggling back to original (Disabled/Default)
  - Do0x20EncodeDisplay: uncheck uses pending.OldValue when present
  - EffectiveConfigFieldDisplay: use pending.OldValue as _valueAtActivation when re-entering with pending
- Effective config: validation errors/warnings in toolbar and save modal, revert from modal
- Custom displays: Do0x20EncodeDisplay (tri-state), KeyOnlyOrValueDisplay
- ExplicitToggleState, config/apply support for do-0x20-encode, option help and tests
2026-03-08 12:52:46 +10:00

208 lines
7.8 KiB
Plaintext

@page "/"
@rendermode InteractiveServer
@namespace DnsmasqWebUI.Components.Pages
@inject IStatusClient StatusClient
@inject IConfigSetClient ConfigSetClient
@inject IReloadClient ReloadClient
@inject IClientSettingsService ClientSettingsService
@inject ISettingsModalService SettingsModalService
@inject INotificationService NotificationService
@inject IOptions<ApplicationOptions> AppOptions
@implements IDisposable
@using DnsmasqWebUI.Infrastructure.Services.UI.Notifications.Abstractions
<PageTitle>Dnsmasq | @AppTitle</PageTitle>
@if (_loading)
{
<p class="text-muted"><em>Loading…</em></p>
}
else if (_error != null)
{
<div class="alert alert-danger">@_error</div>
}
else if (_status != null)
{
<section class="page-section">
<h2 class="page-section-title">Service configuration</h2>
<p class="mb-1"><strong>Config files (load order):</strong></p>
<ul class="list-unstyled small ms-2 mb-0">
@{
var configFileList = _status.ConfigFiles?.Count > 0 ? _status.ConfigFiles : _configSet?.Files;
}
@if (configFileList != null && configFileList.Count > 0)
{
@foreach (var f in configFileList)
{
var isMain = string.Equals(f.Path, _status.MainConfigPath, StringComparison.Ordinal);
var isManaged = f.IsManaged;
var exists = isMain ? _status.MainConfigPathExists : (isManaged ? _status.ManagedFilePathExists : (bool?)null);
<li>
@if (f.IsManaged)
{
<StatusBadge Kind="StatusBadgeKind.Managed" Text="managed" Class="me-1" />
}
else
{
<StatusBadge Kind="StatusBadgeKind.SourceMain" Text="@(f.Source.ToString().ToLowerInvariant())" Class="me-1" />
}
@f.Path
@if (exists == true) { <text> ✓</text> }
@if (exists == false) { <text> @(isManaged ? "(not created yet)" : "(missing)")</text> }
</li>
}
}
else
{
<li><strong>Main config:</strong> @(_status.MainConfigPath ?? "—") @(_status.MainConfigPathExists ? "✓" : "(missing)")</li>
<li><strong>Managed file:</strong> @(_status.ManagedFilePath ?? "—") @(_status.ManagedFilePathExists ? "✓" : "(not created yet)")</li>
}
</ul>
<EffectiveConfigSection Status="_status" Context="EffectiveConfigContext.All" ShowSearchBox="true" OnSaveCompleted="RefreshStatusAsync" />
@if (_status.ReloadCommandConfigured)
{
<div class="d-flex align-items-center gap-2 flex-wrap mt-3">
<button class="btn btn-primary btn-sm" @onclick="ReloadDnsmasq" disabled="@_reloading">
@if (_reloading)
{
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
}
else
{
<i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>
}
@(_reloading ? "Reloading…" : "Reload config")
</button>
</div>
}
<StatusSection RefreshIntervalSeconds="@ClientSettingsFields.ServiceStatusPollingInterval.Value" IsReloading="_reloading" OnReloadRequested="ReloadDnsmasq" ReloadCommandConfigured="_status.ReloadCommandConfigured" OnOpenSettings="OpenServiceStatusSettings" />
<LogsSection Status="_status" RefreshIntervalSeconds="@ClientSettingsFields.RecentLogsPollingInterval.Value" LogsMaxLines="@ClientSettingsFields.RecentLogsMaxLines.Value" LogsAutoScroll="@ClientSettingsFields.RecentLogsAutoScroll.Value" OnOpenSettings="OpenRecentLogsSettings" />
<AppLogsSection RefreshIntervalSeconds="@ClientSettingsFields.AppLogsPollingInterval.Value" LogsMaxLines="@ClientSettingsFields.AppLogsMaxLines.Value" LogsAutoScroll="@ClientSettingsFields.AppLogsAutoScroll.Value" OnOpenSettings="OpenAppLogsSettings" />
</section>
}
@code {
private string AppTitle => AppOptions!.Value!.EffectiveTitle;
private DnsmasqServiceStatus? _status;
private DnsmasqConfigSet? _configSet;
private string? _error;
private bool _loading = true;
private bool _reloading;
private readonly CancellationTokenSource _cts = new();
protected override void OnInitialized()
{
SettingsModalService.SettingsChanged += OnSettingsChanged;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
_loading = true;
try
{
await Task.Delay(1, _cts.Token);
var token = _cts.Token;
await ClientSettingsService.LoadSettingsAsync();
_status = await StatusClient.GetStatusAsync(token);
if (_status != null)
{
try
{
_configSet = await ConfigSetClient.GetConfigSetAsync(token);
}
catch
{
// Config set is optional for Dnsmasq page
}
}
}
catch (OperationCanceledException)
{
// Component disposed or navigated away; ignore
}
catch (Exception ex)
{
_error = ex.Message;
}
finally
{
_loading = false;
StateHasChanged();
}
}
private void OpenServiceStatusSettings() => SettingsModalService.Open(SettingsModalContext.ServicePolling, "Service status polling");
private void OpenRecentLogsSettings() => SettingsModalService.Open(SettingsModalContext.LogsPolling, "Recent logs polling");
private void OpenAppLogsSettings() => SettingsModalService.Open(SettingsModalContext.AppLogsPolling, "App logs polling");
private async void OnSettingsChanged()
{
await InvokeAsync(StateHasChanged);
}
private async Task RefreshStatusAsync()
{
try
{
_status = await StatusClient.GetStatusAsync(_cts.Token);
await InvokeAsync(StateHasChanged);
}
catch (OperationCanceledException) { }
catch (ObjectDisposedException)
{
// Component disposed (e.g. navigated away) while callback was pending; ignore
}
}
private async Task ReloadDnsmasq()
{
if (_status == null || !_status.ReloadCommandConfigured || _reloading) return;
_reloading = true;
try
{
var token = _cts.Token;
var result = await ReloadClient.ReloadAsync(token);
if (result.Success)
{
NotificationService.ShowSuccess("Reloaded.");
}
else
{
var msg = "Reload failed" + (result.ExitCode != 0 ? " (exit " + result.ExitCode + ")" : "") + ".";
if (!string.IsNullOrEmpty(result.StdErr))
msg += " " + result.StdErr;
NotificationService.ShowError(msg);
}
try
{
_status = await StatusClient.GetStatusAsync(token);
}
catch
{
// Keep _reloadMessage; status will refresh on next page load
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
NotificationService.ShowError("Reload failed: " + ex.Message);
}
finally
{
_reloading = false;
}
}
public void Dispose()
{
SettingsModalService.SettingsChanged -= OnSettingsChanged;
_cts.Cancel();
_cts.Dispose();
}
}