mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-22 01:42:08 +10:00
UI polish: update check tooltip date, expand/collapse icons, dialog JS
- NavMenu: show short date in update-check tooltip when last check was not today - Settings modal: expand/collapse all sections with bi-arrows-expand/collapse icons - Effective config: add Expand all button (bi-arrows-expand), same icon style as toolbar - Rename settings-modal.js to dialog.js; add config-option-help.js, JS extension helpers - Config option help modal and client settings/dialog wiring updates
This commit is contained in:
@@ -23,7 +23,6 @@
|
||||
|
||||
<body>
|
||||
<Routes @rendermode="InteractiveServer" />
|
||||
<script src="js/ec-option-help.js"></script>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using DnsmasqWebUI.Extensions;
|
||||
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
@@ -53,7 +54,7 @@ public partial class AppLogsFiltersModal : IAsyncDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
_jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/settings-modal.js");
|
||||
_jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/dialog.js");
|
||||
_dotNetRef = DotNetObjectReference.Create(this);
|
||||
_moduleLoaded = true;
|
||||
}
|
||||
@@ -66,17 +67,17 @@ public partial class AppLogsFiltersModal : IAsyncDisposable
|
||||
{
|
||||
if (!_dialogInitialized)
|
||||
{
|
||||
await _jsModule.InvokeVoidAsync("initDialog", _dialogRef, _dotNetRef);
|
||||
await _jsModule.InvokeVoidAsyncSafe("initDialog", _dialogRef, _dotNetRef);
|
||||
_dialogInitialized = true;
|
||||
}
|
||||
await _jsModule.InvokeVoidAsync("showModal", _dialogRef);
|
||||
await _jsModule.InvokeVoidAsyncSafe("showModal", _dialogRef);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Close()
|
||||
{
|
||||
if (_jsModule != null)
|
||||
await _jsModule.InvokeVoidAsync("closeModal", _dialogRef);
|
||||
await _jsModule.InvokeVoidAsyncSafe("closeModal", _dialogRef);
|
||||
await OnClose.InvokeAsync();
|
||||
}
|
||||
|
||||
@@ -145,10 +146,6 @@ public partial class AppLogsFiltersModal : IAsyncDisposable
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
_dotNetRef?.Dispose();
|
||||
if (_jsModule != null)
|
||||
{
|
||||
try { await _jsModule.DisposeAsync(); }
|
||||
catch { /* ignore */ }
|
||||
}
|
||||
await _jsModule.DisposeAsyncSafe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using DnsmasqWebUI.Extensions;
|
||||
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
using DnsmasqWebUI.Models.Logs;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@@ -137,19 +138,13 @@ public partial class AppLogsSection : IAsyncDisposable
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
if (_logsJs == null) return;
|
||||
try
|
||||
{
|
||||
if (payload.Mode == "replace")
|
||||
await _logsJs.InvokeVoidAsync("replaceLogs", AppLogsPreId, payload.Content, LogsOptions);
|
||||
else
|
||||
await _logsJs.InvokeVoidAsync("appendLogs", AppLogsPreId, payload.Content, LogsOptions);
|
||||
_logsContentReceived = true;
|
||||
SetJustUpdated();
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (JSDisconnectedException) { /* Circuit disconnected; ignore */ }
|
||||
catch (InvalidOperationException) { /* Prerender or circuit disposed; ignore */ }
|
||||
catch (JSException) { /* JS error; ignore */ }
|
||||
if (payload.Mode == "replace")
|
||||
await _logsJs.InvokeVoidAsyncSafe("replaceLogs", AppLogsPreId, payload.Content, LogsOptions);
|
||||
else
|
||||
await _logsJs.InvokeVoidAsyncSafe("appendLogs", AppLogsPreId, payload.Content, LogsOptions);
|
||||
_logsContentReceived = true;
|
||||
SetJustUpdated();
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
catch (ObjectDisposedException) { /* Component disposed; ignore */ }
|
||||
@@ -183,13 +178,7 @@ public partial class AppLogsSection : IAsyncDisposable
|
||||
await _hubConnection.DisposeAsync();
|
||||
_hubConnection = null;
|
||||
}
|
||||
if (_logsJs != null)
|
||||
{
|
||||
try { await _logsJs.DisposeAsync(); }
|
||||
catch (InvalidOperationException ex) { Logger.LogDebug(ex, "AppLogsSection: DisposeAsync skipped (prerender)"); }
|
||||
catch (JSDisconnectedException ex) { Logger.LogDebug(ex, "AppLogsSection: DisposeAsync skipped (circuit disconnected)"); }
|
||||
catch (JSException ex) { Logger.LogDebug(ex, "AppLogsSection: DisposeAsync failed"); }
|
||||
_logsJs = null;
|
||||
}
|
||||
await _logsJs.DisposeAsyncSafe();
|
||||
_logsJs = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,19 +50,33 @@
|
||||
private DotNetObjectReference<ConfigOptionHelpModal>? _dotNetRef;
|
||||
private bool _scrollListenerAdded;
|
||||
private bool _mouseOverModal;
|
||||
private IJSObjectReference? _jsModule;
|
||||
private bool _moduleLoaded;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (Visible && !_scrollListenerAdded)
|
||||
if (firstRender)
|
||||
{
|
||||
try
|
||||
{
|
||||
_jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/config-option-help.js");
|
||||
_moduleLoaded = true;
|
||||
}
|
||||
catch (InvalidOperationException) { /* prerender */ }
|
||||
catch (JSDisconnectedException) { /* circuit disconnected */ }
|
||||
catch (JSException) { /* import failed */ }
|
||||
}
|
||||
|
||||
if (Visible && !_scrollListenerAdded && _moduleLoaded && _jsModule != null)
|
||||
{
|
||||
_scrollListenerAdded = true;
|
||||
_dotNetRef = DotNetObjectReference.Create(this);
|
||||
await JSRuntime.InvokeVoidAsync("addConfigOptionHelpScrollCloseListener", _dotNetRef);
|
||||
await _jsModule.InvokeVoidAsyncSafe("addScrollCloseListener", _dotNetRef);
|
||||
}
|
||||
else if (!Visible && _scrollListenerAdded)
|
||||
else if (!Visible && _scrollListenerAdded && _jsModule != null)
|
||||
{
|
||||
_scrollListenerAdded = false;
|
||||
await JSRuntime.InvokeVoidAsync("removeConfigOptionHelpScrollCloseListener");
|
||||
await _jsModule.InvokeVoidAsyncSafe("removeScrollCloseListener");
|
||||
_dotNetRef?.Dispose();
|
||||
_dotNetRef = null;
|
||||
}
|
||||
@@ -79,13 +93,15 @@
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_scrollListenerAdded)
|
||||
if (_scrollListenerAdded && _jsModule != null)
|
||||
{
|
||||
_scrollListenerAdded = false;
|
||||
try { await JSRuntime.InvokeVoidAsync("removeConfigOptionHelpScrollCloseListener"); } catch { /* best effort */ }
|
||||
await _jsModule.InvokeVoidAsyncSafe("removeScrollCloseListener");
|
||||
_dotNetRef?.Dispose();
|
||||
_dotNetRef = null;
|
||||
}
|
||||
await _jsModule.DisposeAsyncSafe();
|
||||
_jsModule = null;
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
@@ -134,19 +150,16 @@
|
||||
|
||||
private async Task PositionUnderAnchorAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(AnchorId)) return;
|
||||
try
|
||||
if (string.IsNullOrEmpty(AnchorId) || _jsModule == null) return;
|
||||
var rect = await _jsModule.InvokeAsyncSafe<BoundingRect?>("getAnchorRect", AnchorId);
|
||||
if (rect is { } r)
|
||||
{
|
||||
var rect = await JSRuntime.InvokeAsync<BoundingRect?>("getConfigOptionHelpAnchorRect", AnchorId);
|
||||
if (rect is { } r)
|
||||
{
|
||||
var gap = 4;
|
||||
var topPx = r.Bottom + gap;
|
||||
var leftPx = r.Left;
|
||||
_positionStyle = $"position: fixed; top: {topPx}px; left: {leftPx}px; visibility: visible;";
|
||||
}
|
||||
var gap = 4;
|
||||
var topPx = r.Bottom + gap;
|
||||
var leftPx = r.Left;
|
||||
_positionStyle = $"position: fixed; top: {topPx}px; left: {leftPx}px; visibility: visible;";
|
||||
}
|
||||
catch
|
||||
else
|
||||
{
|
||||
_positionStyle = "visibility: visible;";
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
<div class="ec-toolbar">
|
||||
<input type="search" id="ec-search-input" class="form-control form-control-sm" placeholder="Search..." autocomplete="off"
|
||||
@bind="_searchTerm" @bind:event="oninput" @bind:after="StateHasChanged" />
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" @onclick="ExpandAll" title="Expand all sections" aria-label="Expand all sections">
|
||||
<i class="bi bi-arrows-expand" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" @onclick="CollapseAll" title="Collapse all sections" aria-label="Collapse all sections">
|
||||
<i class="bi bi-arrows-collapse" aria-hidden="true"></i>
|
||||
</button>
|
||||
@@ -121,6 +124,13 @@
|
||||
_openPanels.Add(id);
|
||||
}
|
||||
|
||||
private void ExpandAll()
|
||||
{
|
||||
foreach (var view in EffectiveConfigViews.GetViewsForContext(Context))
|
||||
_openPanels.Add(view.SectionId);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void CollapseAll()
|
||||
{
|
||||
_openPanels.Clear();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using DnsmasqWebUI.Extensions;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Logs;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@@ -123,19 +124,13 @@ public partial class LogsSection : IAsyncDisposable
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
if (_logsJs == null) return;
|
||||
try
|
||||
{
|
||||
if (payload.Mode == "replace")
|
||||
await _logsJs.InvokeVoidAsync("replaceLogs", LogsPreId, payload.Content, LogsOptions);
|
||||
else
|
||||
await _logsJs.InvokeVoidAsync("appendLogs", LogsPreId, payload.Content, LogsOptions);
|
||||
_logsContentReceived = true;
|
||||
SetJustUpdated();
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (JSDisconnectedException) { /* Circuit disconnected; ignore */ }
|
||||
catch (InvalidOperationException) { /* Prerender or circuit disposed; ignore */ }
|
||||
catch (JSException) { /* JS error; ignore */ }
|
||||
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 */ }
|
||||
@@ -169,13 +164,7 @@ public partial class LogsSection : IAsyncDisposable
|
||||
await _hubConnection.DisposeAsync();
|
||||
_hubConnection = null;
|
||||
}
|
||||
if (_logsJs != null)
|
||||
{
|
||||
try { await _logsJs.DisposeAsync(); }
|
||||
catch (InvalidOperationException ex) { Logger.LogDebug(ex, "LogsSection: DisposeAsync skipped (prerender)"); }
|
||||
catch (JSDisconnectedException ex) { Logger.LogDebug(ex, "LogsSection: DisposeAsync skipped (circuit disconnected)"); }
|
||||
catch (JSException ex) { Logger.LogDebug(ex, "LogsSection: DisposeAsync failed"); }
|
||||
_logsJs = null;
|
||||
}
|
||||
await _logsJs.DisposeAsyncSafe();
|
||||
_logsJs = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,11 @@
|
||||
return "Checking…";
|
||||
var last = UpdateCheckService.LastCheckTime;
|
||||
if (last.HasValue)
|
||||
return $"No new update available. Checked at {last.Value.ToLocalTime():t}. Check again.";
|
||||
{
|
||||
var local = last.Value.ToLocalTime();
|
||||
var format = local.Date == DateTime.Now.Date ? "t" : "g"; // time only today, short date+time otherwise
|
||||
return $"No new update available. Checked at {local.ToString(format)}. Check again.";
|
||||
}
|
||||
return "Check for updates";
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,17 @@
|
||||
@if (SettingsContext == SettingsModalContext.All)
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Search settings</label>
|
||||
<div class="d-flex align-items-center gap-2 mb-1">
|
||||
<label class="form-label mb-0">Search settings</label>
|
||||
<div class="d-flex gap-1 ms-auto" role="group" aria-label="Expand or collapse all sections">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary settings-expand-collapse-btn" title="Expand all sections" aria-label="Expand all sections" @onclick="ExpandAllGroups">
|
||||
<i class="bi bi-arrows-expand" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary settings-expand-collapse-btn" title="Collapse all sections" aria-label="Collapse all sections" @onclick="CollapseAllGroups">
|
||||
<i class="bi bi-arrows-collapse" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="search" class="form-control" placeholder="Search..." autocomplete="off"
|
||||
@bind="_searchTerm" @bind:event="oninput" @bind:after="FilterSettings" />
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using DnsmasqWebUI.Extensions;
|
||||
using DnsmasqWebUI.Models.Client;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@@ -55,7 +56,7 @@ public partial class SettingsModal : IAsyncDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
_jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/settings-modal.js");
|
||||
_jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/dialog.js");
|
||||
_dotNetRef = DotNetObjectReference.Create(this);
|
||||
_moduleLoaded = true;
|
||||
}
|
||||
@@ -68,10 +69,10 @@ public partial class SettingsModal : IAsyncDisposable
|
||||
{
|
||||
if (!_dialogInitialized)
|
||||
{
|
||||
await _jsModule.InvokeVoidAsync("initDialog", _dialogRef, _dotNetRef);
|
||||
await _jsModule.InvokeVoidAsyncSafe("initDialog", _dialogRef, _dotNetRef);
|
||||
_dialogInitialized = true;
|
||||
}
|
||||
await _jsModule.InvokeVoidAsync("showModal", _dialogRef);
|
||||
await _jsModule.InvokeVoidAsyncSafe("showModal", _dialogRef);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +98,19 @@ public partial class SettingsModal : IAsyncDisposable
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ExpandAllGroups()
|
||||
{
|
||||
foreach (var g in GetVisibleGroups())
|
||||
_expandedGroupIds.Add(g.Id);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void CollapseAllGroups()
|
||||
{
|
||||
_expandedGroupIds.Clear();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private bool ShouldShowSection(string key)
|
||||
{
|
||||
var keys = SettingsModalSections.GetSectionKeysForContext(SettingsContext);
|
||||
@@ -125,13 +139,15 @@ public partial class SettingsModal : IAsyncDisposable
|
||||
}
|
||||
|
||||
await ClientSettingsService.SaveSettingsAsync(ClientSettingsFields.ToDto());
|
||||
await (_jsModule?.InvokeVoidAsync("closeModal", _dialogRef) ?? ValueTask.CompletedTask);
|
||||
if (_jsModule != null)
|
||||
await _jsModule.InvokeVoidAsyncSafe("closeModal", _dialogRef);
|
||||
await OnClose.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task Close()
|
||||
{
|
||||
await (_jsModule?.InvokeVoidAsync("closeModal", _dialogRef) ?? ValueTask.CompletedTask);
|
||||
if (_jsModule != null)
|
||||
await _jsModule.InvokeVoidAsyncSafe("closeModal", _dialogRef);
|
||||
await OnClose.InvokeAsync();
|
||||
}
|
||||
|
||||
@@ -147,14 +163,8 @@ public partial class SettingsModal : IAsyncDisposable
|
||||
/// </summary>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_jsModule != null)
|
||||
{
|
||||
try { await _jsModule.DisposeAsync(); }
|
||||
catch (InvalidOperationException ex) { Logger.LogDebug(ex, "SettingsModal: DisposeAsync skipped (prerender)"); }
|
||||
catch (JSDisconnectedException ex) { Logger.LogDebug(ex, "SettingsModal: DisposeAsync skipped (circuit disconnected)"); }
|
||||
catch (JSException ex) { Logger.LogDebug(ex, "SettingsModal: DisposeAsync failed"); }
|
||||
_jsModule = null;
|
||||
}
|
||||
await _jsModule.DisposeAsyncSafe();
|
||||
_jsModule = null;
|
||||
_dotNetRef?.Dispose();
|
||||
_dotNetRef = null;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,15 @@
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.settings-expand-collapse-btn {
|
||||
padding: 0.25rem 0.4rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.settings-expand-collapse-btn .bi {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.settings-modal-dialog .modal-footer {
|
||||
padding: 1rem 1.25rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.125);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using DnsmasqWebUI
|
||||
@using DnsmasqWebUI.Extensions
|
||||
@using DnsmasqWebUI.Models.Client
|
||||
@using DnsmasqWebUI.Models.Client.Abstractions
|
||||
@using DnsmasqWebUI.Infrastructure.Services.Abstractions
|
||||
|
||||
Reference in New Issue
Block a user