mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-22 01:42:08 +10:00
Refactor: Bootstrap 5.3.8, Bootstrap Icons, Client/Http/Configuration layout
- Update Bootstrap to 5.3.8, add Bootstrap Icons; replace custom SVGs - Fix settings modal: load editing state only when modal opens (avoid refresh overwrite) - Home: StatusSection and LogsSection components with own timers - Move HTTP clients under Client/Http with Abstractions namespace pattern - Rename Options to Configuration; remove empty Options dir - Components: Modals/, Home/; consistent Abstractions (Services, Client.Services, Client.Http)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
using DnsmasqWebUI.Models;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http.Abstractions;
|
||||
|
||||
/// <summary>Typed client for GET api/config/set.</summary>
|
||||
public interface IConfigSetClient
|
||||
{
|
||||
Task<DnsmasqConfigSet> GetConfigSetAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using DnsmasqWebUI.Models;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http.Abstractions;
|
||||
|
||||
/// <summary>Typed client for GET/PUT api/dhcp/hosts.</summary>
|
||||
public interface IDhcpHostsClient
|
||||
{
|
||||
Task<IReadOnlyList<DhcpHostEntry>> GetDhcpHostsAsync(CancellationToken ct = default);
|
||||
Task<SaveWithReloadResult> SaveDhcpHostsAsync(IReadOnlyList<DhcpHostEntry> entries, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using DnsmasqWebUI.Models;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http.Abstractions;
|
||||
|
||||
/// <summary>Typed client for GET/PUT api/hosts.</summary>
|
||||
public interface IHostsClient
|
||||
{
|
||||
Task<IReadOnlyList<HostEntry>> GetHostsAsync(CancellationToken ct = default);
|
||||
Task<SaveWithReloadResult> SaveHostsAsync(IReadOnlyList<HostEntry> entries, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using DnsmasqWebUI.Models;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http.Abstractions;
|
||||
|
||||
/// <summary>Typed client for GET api/leases.</summary>
|
||||
public interface ILeasesClient
|
||||
{
|
||||
Task<LeasesResult> GetLeasesAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http.Abstractions;
|
||||
|
||||
/// <summary>Typed client for POST api/reload.</summary>
|
||||
public interface IReloadClient
|
||||
{
|
||||
Task<ReloadResult> ReloadAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using DnsmasqWebUI.Models;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http.Abstractions;
|
||||
|
||||
/// <summary>Typed client for GET api/status.</summary>
|
||||
public interface IStatusClient
|
||||
{
|
||||
Task<DnsmasqServiceStatus> GetStatusAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Models;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http;
|
||||
|
||||
public sealed class ConfigSetClient : IConfigSetClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public ConfigSetClient(HttpClient http) => _http = http;
|
||||
|
||||
public async Task<DnsmasqConfigSet> GetConfigSetAsync(CancellationToken ct = default) =>
|
||||
await _http.GetFromJsonAsync<DnsmasqConfigSet>("api/config/set", ct)
|
||||
?? throw new InvalidOperationException("Unexpected null from api/config/set.");
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Models;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http;
|
||||
|
||||
public sealed class DhcpHostsClient : IDhcpHostsClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public DhcpHostsClient(HttpClient http) => _http = http;
|
||||
|
||||
public async Task<IReadOnlyList<DhcpHostEntry>> GetDhcpHostsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var list = await _http.GetFromJsonAsync<List<DhcpHostEntry>>("api/dhcp/hosts", ct);
|
||||
return list ?? new List<DhcpHostEntry>();
|
||||
}
|
||||
|
||||
public async Task<SaveWithReloadResult> SaveDhcpHostsAsync(IReadOnlyList<DhcpHostEntry> entries, CancellationToken ct = default)
|
||||
{
|
||||
var response = await _http.PutAsJsonAsync("api/dhcp/hosts", entries, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<SaveWithReloadResult>(ct)
|
||||
?? throw new InvalidOperationException("Unexpected null from api/dhcp/hosts.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Models;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http;
|
||||
|
||||
public sealed class HostsClient : IHostsClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public HostsClient(HttpClient http) => _http = http;
|
||||
|
||||
public async Task<IReadOnlyList<HostEntry>> GetHostsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var list = await _http.GetFromJsonAsync<List<HostEntry>>("api/hosts", ct);
|
||||
return list ?? new List<HostEntry>();
|
||||
}
|
||||
|
||||
public async Task<SaveWithReloadResult> SaveHostsAsync(IReadOnlyList<HostEntry> entries, CancellationToken ct = default)
|
||||
{
|
||||
var response = await _http.PutAsJsonAsync("api/hosts", entries, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<SaveWithReloadResult>(ct)
|
||||
?? throw new InvalidOperationException("Unexpected null from api/hosts.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Models;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http;
|
||||
|
||||
public sealed class LeasesClient : ILeasesClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public LeasesClient(HttpClient http) => _http = http;
|
||||
|
||||
public async Task<LeasesResult> GetLeasesAsync(CancellationToken ct = default) =>
|
||||
await _http.GetFromJsonAsync<LeasesResult>("api/leases", ct)
|
||||
?? throw new InvalidOperationException("Unexpected null from api/leases.");
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http;
|
||||
|
||||
public sealed class ReloadClient : IReloadClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public ReloadClient(HttpClient http) => _http = http;
|
||||
|
||||
public async Task<ReloadResult> ReloadAsync(CancellationToken ct = default)
|
||||
{
|
||||
var response = await _http.PostAsync("api/reload", null, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ReloadResult>(ct)
|
||||
?? throw new InvalidOperationException("Unexpected null from api/reload.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Net;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http;
|
||||
|
||||
/// <summary>
|
||||
/// DelegatingHandler that rewrites relative request URIs to the current request's scheme, host, and path base.
|
||||
/// Used so Blazor/API callers can use relative paths (e.g. "/api/status") and hit the same host.
|
||||
/// Resolved in the same scope as the code that requested the HttpClient (e.g. Blazor component),
|
||||
/// so IHttpContextAccessor has the current request when available.
|
||||
/// </summary>
|
||||
public sealed class SameHostBaseAddressHandler(IHttpContextAccessor httpContextAccessor) : DelegatingHandler, IApplicationScopedConcrete
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var context = httpContextAccessor.HttpContext;
|
||||
if (context != null && request.RequestUri != null)
|
||||
{
|
||||
var baseUri = new Uri($"{context.Request.Scheme}://{context.Request.Host.Value}{context.Request.PathBase.Value ?? ""}");
|
||||
request.RequestUri = request.RequestUri.IsAbsoluteUri
|
||||
? new Uri(baseUri, request.RequestUri.PathAndQuery)
|
||||
: new Uri(baseUri, request.RequestUri);
|
||||
}
|
||||
|
||||
return base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Models;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http;
|
||||
|
||||
public sealed class StatusClient : IStatusClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public StatusClient(HttpClient http) => _http = http;
|
||||
|
||||
public async Task<DnsmasqServiceStatus> GetStatusAsync(CancellationToken ct = default) =>
|
||||
await _http.GetFromJsonAsync<DnsmasqServiceStatus>("api/status", ct)
|
||||
?? throw new InvalidOperationException("Unexpected null from api/status.");
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace DnsmasqWebUI.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Client-side preferences persisted in browser local storage.
|
||||
/// </summary>
|
||||
public class ClientSettings
|
||||
{
|
||||
/// <summary>Polling interval for the service status block (seconds).</summary>
|
||||
public int ServiceStatusPollingIntervalSeconds { get; set; } = 15;
|
||||
|
||||
/// <summary>Polling interval for the recent logs block (seconds).</summary>
|
||||
public int RecentLogsPollingIntervalSeconds { get; set; } = 5;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace DnsmasqWebUI.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Which subset of client settings to show in the settings modal.
|
||||
/// </summary>
|
||||
public enum SettingsModalContext
|
||||
{
|
||||
/// <summary>Show all settings with optional search.</summary>
|
||||
All,
|
||||
|
||||
/// <summary>Show only service status polling interval.</summary>
|
||||
ServicePolling,
|
||||
|
||||
/// <summary>Show only recent logs polling interval.</summary>
|
||||
LogsPolling
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using DnsmasqWebUI.Client.Models;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and saves client-side preferences (e.g. polling intervals) to browser local storage.
|
||||
/// Registered as scoped via assembly scanning (<see cref="IApplicationScopedService"/>).
|
||||
/// </summary>
|
||||
public interface IClientSettingsService : IApplicationScopedService
|
||||
{
|
||||
/// <summary>Loads settings from local storage; returns defaults if missing or invalid.</summary>
|
||||
Task<ClientSettings> LoadSettingsAsync();
|
||||
|
||||
/// <summary>Persists settings to local storage.</summary>
|
||||
Task SaveSettingsAsync(ClientSettings settings);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Text.Json;
|
||||
using DnsmasqWebUI.Client.Models;
|
||||
using DnsmasqWebUI.Client.Services.Abstractions;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and saves client-side settings to browser local storage via JS interop.
|
||||
/// Scoped per Blazor circuit; disposes the JS module when the scope is disposed.
|
||||
/// </summary>
|
||||
public sealed class ClientSettingsService : IClientSettingsService, IAsyncDisposable
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private IJSObjectReference? _module;
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
public ClientSettingsService(IJSRuntime jsRuntime) => _jsRuntime = jsRuntime;
|
||||
|
||||
private async Task<IJSObjectReference> GetModuleAsync()
|
||||
{
|
||||
if (_module != null) return _module;
|
||||
_module = await _jsRuntime.InvokeAsync<IJSObjectReference>(
|
||||
"import", "./js/client-settings-storage.js");
|
||||
return _module;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ClientSettings> LoadSettingsAsync()
|
||||
{
|
||||
var module = await GetModuleAsync();
|
||||
var json = await module.InvokeAsync<string?>("getItem");
|
||||
if (string.IsNullOrWhiteSpace(json)) return new ClientSettings();
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<ClientSettings>(json, JsonOptions) ?? new ClientSettings();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new ClientSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SaveSettingsAsync(ClientSettings settings)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(settings, JsonOptions);
|
||||
var module = await GetModuleAsync();
|
||||
await module.InvokeVoidAsync("setItem", json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the JS module reference. Called when the Blazor circuit scope is disposed.
|
||||
/// </summary>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_module == null) return;
|
||||
try
|
||||
{
|
||||
await _module.DisposeAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_module = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user