mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-04 04:18:13 +10:00
- Hosts: delete confirmation modal, shift range select, keyboard shortcuts, effective-name suffix tooltips, conditional modern header actions - Align EffectiveNames on rows with scoped domain= + pending preview; unified hosts API uses full DomainValues from config snapshot - Harden Blazor JS interop (safe helpers, IAsyncDisposable, disposal order) - Effective config: domain multi-value display/editor, parsers, tests - Misc: docs/testdata/docker script updates
171 lines
5.4 KiB
C#
171 lines
5.4 KiB
C#
using DnsmasqWebUI.Extensions.Interop;
|
|
using DnsmasqWebUI.Models.Dnsmasq;
|
|
using DnsmasqWebUI.Models.Logs;
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.AspNetCore.SignalR.Client;
|
|
using Microsoft.JSInterop;
|
|
|
|
namespace DnsmasqWebUI.Components.Dnsmasq.Logs;
|
|
|
|
/// <summary>
|
|
/// Recent logs block: connects to LogsHub, polls at interval via RequestDnsmasqLogs,
|
|
/// receives DnsmasqLogsUpdate pushes, updates DOM via JS interop.
|
|
/// </summary>
|
|
public partial class LogsSection : IAsyncDisposable
|
|
{
|
|
private const string LogsPreId = "dnsmasq-logs-pre";
|
|
|
|
private DnsmasqServiceStatus? _status;
|
|
private bool _refreshing;
|
|
private bool _justUpdated;
|
|
private bool _logsContentReceived;
|
|
private int _intervalSeconds;
|
|
private HubConnection? _hubConnection;
|
|
private IJSObjectReference? _logsJs;
|
|
private Timer? _pollTimer;
|
|
private Timer? _justUpdatedResetTimer;
|
|
private readonly CancellationTokenSource _cts = new();
|
|
|
|
[Parameter] public DnsmasqServiceStatus? Status { get; set; }
|
|
|
|
[Parameter] public int RefreshIntervalSeconds { get; set; } = 15;
|
|
|
|
[Parameter] public int LogsMaxLines { get; set; } = 500;
|
|
|
|
[Parameter] public bool LogsAutoScroll { get; set; } = true;
|
|
|
|
[Parameter] public EventCallback OnOpenSettings { get; set; }
|
|
|
|
private object LogsOptions => new { maxLines = LogsMaxLines, autoScroll = LogsAutoScroll };
|
|
|
|
private string _initialPlaceholder => _hubConnection?.State == HubConnectionState.Connected ? "Waiting for logs…" : "Connecting…";
|
|
|
|
protected override void OnParametersSet()
|
|
{
|
|
_status = Status;
|
|
if (RefreshIntervalSeconds == _intervalSeconds) return;
|
|
_intervalSeconds = RefreshIntervalSeconds;
|
|
RestartPollTimer();
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (!firstRender) return;
|
|
_intervalSeconds = RefreshIntervalSeconds;
|
|
_status = Status;
|
|
|
|
var hubUri = new Uri(new Uri(Navigation.BaseUri), "hubs/logs").ToString();
|
|
|
|
_hubConnection = new HubConnectionBuilder()
|
|
.WithUrl(hubUri)
|
|
.WithAutomaticReconnect()
|
|
.Build();
|
|
|
|
_hubConnection.On<LogsUpdatePayload>("DnsmasqLogsUpdate", OnDnsmasqLogsUpdate);
|
|
|
|
_logsJs = await JSRuntime.InvokeAsyncSafe<IJSObjectReference>("import", default, "./js/logs.js");
|
|
if (_logsJs == null)
|
|
{
|
|
Logger.LogDebug("LogsSection: logs JS module unavailable (prerender, disconnect, or import failure)");
|
|
return;
|
|
}
|
|
|
|
await _hubConnection.StartAsync(_cts.Token);
|
|
RestartPollTimer();
|
|
|
|
// Initial request
|
|
await RequestRefreshAsync();
|
|
}
|
|
|
|
private void RestartPollTimer()
|
|
{
|
|
_pollTimer?.Dispose();
|
|
_pollTimer = new Timer(
|
|
_ => _ = InvokeAsync(PollTick),
|
|
null,
|
|
TimeSpan.FromSeconds(_intervalSeconds),
|
|
TimeSpan.FromSeconds(_intervalSeconds));
|
|
}
|
|
|
|
private async Task PollTick()
|
|
{
|
|
if (_cts.Token.IsCancellationRequested || _hubConnection?.State != HubConnectionState.Connected)
|
|
return;
|
|
await RequestRefreshAsync();
|
|
}
|
|
|
|
private async Task RequestRefreshAsync()
|
|
{
|
|
if (_hubConnection?.State != HubConnectionState.Connected)
|
|
return;
|
|
_refreshing = true;
|
|
StateHasChanged();
|
|
try
|
|
{
|
|
await _hubConnection.InvokeAsync("RequestDnsmasqLogs", _cts.Token);
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
catch (Exception)
|
|
{
|
|
// Don't overwrite on background refresh
|
|
}
|
|
finally
|
|
{
|
|
_refreshing = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async void OnDnsmasqLogsUpdate(LogsUpdatePayload payload)
|
|
{
|
|
try
|
|
{
|
|
await InvokeAsync(async () =>
|
|
{
|
|
if (_logsJs == null) return;
|
|
if (payload.Mode == "replace")
|
|
await _logsJs.InvokeVoidAsyncSafe("replaceLogs", LogsPreId, payload.Content, LogsOptions);
|
|
else
|
|
await _logsJs.InvokeVoidAsyncSafe("appendLogs", LogsPreId, payload.Content, LogsOptions);
|
|
_logsContentReceived = true;
|
|
SetJustUpdated();
|
|
StateHasChanged();
|
|
});
|
|
}
|
|
catch (ObjectDisposedException) { /* Component disposed; ignore */ }
|
|
catch (InvalidOperationException) { /* Circuit disconnected; ignore */ }
|
|
catch (JSDisconnectedException) { /* Circuit gone before InvokeAsync completed */ }
|
|
}
|
|
|
|
private void SetJustUpdated()
|
|
{
|
|
_justUpdated = true;
|
|
_justUpdatedResetTimer?.Dispose();
|
|
_justUpdatedResetTimer = new Timer(_ =>
|
|
{
|
|
_justUpdatedResetTimer?.Dispose();
|
|
_justUpdatedResetTimer = null;
|
|
_ = InvokeAsync(() =>
|
|
{
|
|
_justUpdated = false;
|
|
StateHasChanged();
|
|
});
|
|
}, null, TimeSpan.FromSeconds(2), Timeout.InfiniteTimeSpan);
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
_pollTimer?.Dispose();
|
|
_justUpdatedResetTimer?.Dispose();
|
|
_cts.Cancel();
|
|
_cts.Dispose();
|
|
if (_hubConnection != null)
|
|
{
|
|
await _hubConnection.DisposeAsync();
|
|
_hubConnection = null;
|
|
}
|
|
await _logsJs.DisposeAsyncSafe();
|
|
_logsJs = null;
|
|
}
|
|
}
|