mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-21 07:15:58 +10:00
Refactor client settings fields, fix logs overwrite, improve settings modal
- Replace FieldBounds with generic ClientSettingsField<T> abstraction - Add IClientSettingsField, ClientSettingsMinMaxField, ClientSettingsBoolField - Put abstractions in Models/Client/Abstractions namespace - Fields hold values; CoerceValue for clamping on set - ClientSettingsService hydrates fields on load; ToDto for save - Settings modal binds directly to field values, collapsible only for All context - Add LogsPreView with ShouldRender freeze to prevent Blazor overwriting JS-owned logs DOM - Add left margin to static group heading in single-section mode
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
namespace DnsmasqWebUI.Models.Client.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Generic base for client settings fields. Holds the value and provides coercion on set.
|
||||
/// </summary>
|
||||
public abstract class ClientSettingsField<T> : IClientSettingsField
|
||||
{
|
||||
public string DisplayName { get; }
|
||||
private T _value = default!;
|
||||
|
||||
public virtual T Value
|
||||
{
|
||||
get => _value;
|
||||
set => _value = CoerceValue(value);
|
||||
}
|
||||
|
||||
object? IClientSettingsField.Value
|
||||
{
|
||||
get => Value;
|
||||
set => Value = (T)value!;
|
||||
}
|
||||
|
||||
public abstract string? Validate(T value);
|
||||
|
||||
string? IClientSettingsField.Validate() => Validate(Value);
|
||||
|
||||
protected virtual T CoerceValue(T value) => value;
|
||||
|
||||
protected ClientSettingsField(string displayName)
|
||||
{
|
||||
DisplayName = displayName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace DnsmasqWebUI.Models.Client.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Non-generic interface for heterogeneous iteration over client settings fields.
|
||||
/// </summary>
|
||||
public interface IClientSettingsField
|
||||
{
|
||||
string DisplayName { get; }
|
||||
object? Value { get; set; }
|
||||
string? Validate();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using DnsmasqWebUI.Models.Client.Abstractions;
|
||||
|
||||
namespace DnsmasqWebUI.Models.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Boolean field. No validation or coercion.
|
||||
/// </summary>
|
||||
public sealed class ClientSettingsBoolField : ClientSettingsField<bool>
|
||||
{
|
||||
public ClientSettingsBoolField(string displayName) : base(displayName) { }
|
||||
|
||||
public override string? Validate(bool value) => null;
|
||||
}
|
||||
@@ -1,23 +1,66 @@
|
||||
using DnsmasqWebUI.Models.Client.Abstractions;
|
||||
|
||||
namespace DnsmasqWebUI.Models.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Per-field metadata for client settings: bounds, display names for validation errors.
|
||||
/// Used by SettingsModal (validation on save) and by pages/components (defensive clamp when loading from storage).
|
||||
/// Static field definitions and hydration/serialization helpers.
|
||||
/// </summary>
|
||||
public static class ClientSettingsFields
|
||||
{
|
||||
public sealed record FieldBounds(int Min, int Max, string DisplayName)
|
||||
static ClientSettingsFields()
|
||||
{
|
||||
public bool IsValid(int value) => value >= Min && value <= Max;
|
||||
public int Clamp(int value) => Math.Clamp(value, Min, Max);
|
||||
public string? Validate(int value) =>
|
||||
IsValid(value) ? null : $"{DisplayName} must be between {Min} and {Max} seconds. You entered {value}.";
|
||||
HydrateFrom(new ClientSettings());
|
||||
}
|
||||
|
||||
public static readonly FieldBounds ServiceStatusPollingInterval = new(5, 300, "Service status polling interval");
|
||||
public static readonly FieldBounds RecentLogsPollingInterval = new(5, 300, "Recent logs polling interval");
|
||||
public static readonly FieldBounds AppLogsPollingInterval = new(5, 300, "App logs polling interval");
|
||||
public static readonly FieldBounds LeasesPollingInterval = new(5, 300, "DHCP leases refresh interval");
|
||||
public static readonly FieldBounds RecentLogsMaxLines = new(100, 2000, "Recent logs max lines");
|
||||
public static readonly FieldBounds AppLogsMaxLines = new(100, 2000, "App logs max lines");
|
||||
public static readonly ClientSettingsMinMaxField ServiceStatusPollingInterval = new("Service status polling interval", 5, 300);
|
||||
public static readonly ClientSettingsMinMaxField RecentLogsPollingInterval = new("Recent logs polling interval", 5, 300);
|
||||
public static readonly ClientSettingsMinMaxField AppLogsPollingInterval = new("App logs polling interval", 5, 300);
|
||||
public static readonly ClientSettingsMinMaxField LeasesPollingInterval = new("DHCP leases refresh interval", 5, 300);
|
||||
public static readonly ClientSettingsMinMaxField RecentLogsMaxLines = new("Recent logs max lines", 100, 2000, "lines");
|
||||
public static readonly ClientSettingsMinMaxField AppLogsMaxLines = new("App logs max lines", 100, 2000, "lines");
|
||||
public static readonly ClientSettingsBoolField RecentLogsAutoScroll = new("Recent logs auto-scroll");
|
||||
public static readonly ClientSettingsBoolField AppLogsAutoScroll = new("App logs auto-scroll");
|
||||
|
||||
private static readonly (string Section, IClientSettingsField Field)[] ValidationMap =
|
||||
[
|
||||
(SettingsModalSections.ServiceStatus, ServiceStatusPollingInterval),
|
||||
(SettingsModalSections.Logs, RecentLogsPollingInterval),
|
||||
(SettingsModalSections.AppLogs, AppLogsPollingInterval),
|
||||
(SettingsModalSections.Leases, LeasesPollingInterval),
|
||||
(SettingsModalSections.RecentLogsDisplay, RecentLogsMaxLines),
|
||||
(SettingsModalSections.RecentLogsDisplay, RecentLogsAutoScroll),
|
||||
(SettingsModalSections.AppLogsDisplay, AppLogsMaxLines),
|
||||
(SettingsModalSections.AppLogsDisplay, AppLogsAutoScroll),
|
||||
];
|
||||
|
||||
public static IReadOnlyList<IClientSettingsField> All => [ServiceStatusPollingInterval, RecentLogsPollingInterval, AppLogsPollingInterval, LeasesPollingInterval, RecentLogsMaxLines, AppLogsMaxLines, RecentLogsAutoScroll, AppLogsAutoScroll];
|
||||
|
||||
public static IReadOnlyList<(string Section, IClientSettingsField Field)> ValidationChecks => ValidationMap;
|
||||
|
||||
public static void HydrateFrom(ClientSettings dto)
|
||||
{
|
||||
ServiceStatusPollingInterval.Value = dto.ServiceStatusPollingIntervalSeconds;
|
||||
RecentLogsPollingInterval.Value = dto.RecentLogsPollingIntervalSeconds;
|
||||
AppLogsPollingInterval.Value = dto.AppLogsPollingIntervalSeconds;
|
||||
LeasesPollingInterval.Value = dto.LeasesPollingIntervalSeconds;
|
||||
RecentLogsMaxLines.Value = dto.RecentLogsMaxLines;
|
||||
RecentLogsAutoScroll.Value = dto.RecentLogsAutoScroll;
|
||||
AppLogsMaxLines.Value = dto.AppLogsMaxLines;
|
||||
AppLogsAutoScroll.Value = dto.AppLogsAutoScroll;
|
||||
}
|
||||
|
||||
public static ClientSettings ToDto()
|
||||
{
|
||||
return new ClientSettings
|
||||
{
|
||||
ServiceStatusPollingIntervalSeconds = ServiceStatusPollingInterval.Value,
|
||||
RecentLogsPollingIntervalSeconds = RecentLogsPollingInterval.Value,
|
||||
AppLogsPollingIntervalSeconds = AppLogsPollingInterval.Value,
|
||||
LeasesPollingIntervalSeconds = LeasesPollingInterval.Value,
|
||||
RecentLogsMaxLines = RecentLogsMaxLines.Value,
|
||||
RecentLogsAutoScroll = RecentLogsAutoScroll.Value,
|
||||
AppLogsMaxLines = AppLogsMaxLines.Value,
|
||||
AppLogsAutoScroll = AppLogsAutoScroll.Value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using DnsmasqWebUI.Models.Client.Abstractions;
|
||||
|
||||
namespace DnsmasqWebUI.Models.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Min/max bounded integer field with optional clamping on set.
|
||||
/// </summary>
|
||||
public sealed class ClientSettingsMinMaxField : ClientSettingsField<int>
|
||||
{
|
||||
public int Min { get; }
|
||||
public int Max { get; }
|
||||
public string Unit { get; }
|
||||
public bool AllowClamp { get; }
|
||||
|
||||
public ClientSettingsMinMaxField(string displayName, int min, int max, string unit = "seconds", bool allowClamp = true)
|
||||
: base(displayName)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
Unit = unit;
|
||||
AllowClamp = allowClamp;
|
||||
}
|
||||
|
||||
public override string? Validate(int value)
|
||||
{
|
||||
if (value < Min || value > Max)
|
||||
return $"{DisplayName} must be between {Min} and {Max} {Unit}. You entered {value}.";
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override int CoerceValue(int value) =>
|
||||
AllowClamp ? Math.Clamp(value, Min, Max) : value;
|
||||
}
|
||||
Reference in New Issue
Block a user