mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-03 22:12:00 +10:00
- 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
208 lines
7.8 KiB
Plaintext
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();
|
|
}
|
|
}
|