Refactor settings modal to use ISettingsModalService and SettingsModalSections

- Replace SettingsModalContext with SettingsModalSections
- Add SettingsModalService for modal state management
- Add StatusSection.razor.css for status-dot styles
- Update layout, NavMenu, and modal components
- Update tests for config/settings changes
This commit is contained in:
2026-02-09 14:59:01 +10:00
parent d06d6ef6c9
commit b70aa02017
26 changed files with 754 additions and 317 deletions
@@ -16,33 +16,36 @@
{
<div class="mb-3">
<label class="form-label">Search settings</label>
<input type="search" class="form-control" placeholder="Search..."
@bind="_searchTerm" @bind:event="oninput" />
<input type="search" class="form-control" placeholder="Search..." autocomplete="off"
@bind="_searchTerm" @bind:event="oninput" @bind:after="FilterSettings" />
</div>
}
@if (ShouldShowSection("ServiceStatus"))
@if (ShouldShowSection(SettingsModalSections.ServiceStatus))
{
var meta = SettingsModalSections.All[SettingsModalSections.ServiceStatus];
<section class="mb-4">
<h6 class="text-muted">Service status</h6>
<label for="status-interval" class="form-label">Polling interval (seconds)</label>
<h6 class="text-muted">@meta.DisplayName</h6>
<label for="status-interval" class="form-label">@meta.FieldLabel</label>
<input id="status-interval" type="number" class="form-control" min="5" step="1"
@bind="_editingSettings.ServiceStatusPollingIntervalSeconds" />
</section>
}
@if (ShouldShowSection("Logs"))
@if (ShouldShowSection(SettingsModalSections.Logs))
{
var meta = SettingsModalSections.All[SettingsModalSections.Logs];
<section class="mb-4">
<h6 class="text-muted">Recent logs</h6>
<label for="logs-interval" class="form-label">Polling interval (seconds)</label>
<h6 class="text-muted">@meta.DisplayName</h6>
<label for="logs-interval" class="form-label">@meta.FieldLabel</label>
<input id="logs-interval" type="number" class="form-control" min="5" step="1"
@bind="_editingSettings.RecentLogsPollingIntervalSeconds" />
</section>
}
@if (ShouldShowSection("Leases"))
@if (ShouldShowSection(SettingsModalSections.Leases))
{
var meta = SettingsModalSections.All[SettingsModalSections.Leases];
<section class="mb-4">
<h6 class="text-muted">DHCP leases</h6>
<label for="leases-interval" class="form-label">Refresh interval (seconds)</label>
<h6 class="text-muted">@meta.DisplayName</h6>
<label for="leases-interval" class="form-label">@meta.FieldLabel</label>
<input id="leases-interval" type="number" class="form-control" min="5" step="1"
@bind="_editingSettings.LeasesPollingIntervalSeconds" />
</section>
@@ -38,6 +38,7 @@ public partial class SettingsModal : IAsyncDisposable
// Only load from storage when the modal opens; parent re-renders (e.g. refresh timer) must not overwrite in-progress edits
if (justOpened)
{
_searchTerm = string.Empty;
_editingSettings = await ClientSettingsService.LoadSettingsAsync();
_editingSettings = new ClientSettings
{
@@ -68,27 +69,14 @@ public partial class SettingsModal : IAsyncDisposable
}
}
private void FilterSettings() => StateHasChanged();
private bool ShouldShowSection(string key)
{
if (SettingsContext == SettingsModalContext.ServicePolling)
return key == "ServiceStatus";
if (SettingsContext == SettingsModalContext.LogsPolling)
return key == "Logs";
if (SettingsContext == SettingsModalContext.LeasesPolling)
return key == "Leases";
if (SettingsContext == SettingsModalContext.All)
{
if (string.IsNullOrWhiteSpace(_searchTerm)) return true;
var term = _searchTerm.Trim();
return key switch
{
"ServiceStatus" => "service status polling".Contains(term, StringComparison.OrdinalIgnoreCase),
"Logs" => "recent logs polling".Contains(term, StringComparison.OrdinalIgnoreCase),
"Leases" => "dhcp leases refresh polling".Contains(term, StringComparison.OrdinalIgnoreCase),
_ => false
};
}
return false;
var focusedKey = SettingsModalSections.GetSectionKeyForContext(SettingsContext);
if (focusedKey != null)
return string.Equals(key, focusedKey, StringComparison.OrdinalIgnoreCase);
return SettingsModalSections.MatchesSearch(key, _searchTerm);
}
private async Task Save()