fix(central-ui): resolve CentralUI-007..014 — nav authz, UTC date filters, disposal guards, N+1 fix, async script analysis
This commit is contained in:
@@ -194,15 +194,12 @@
|
||||
try
|
||||
{
|
||||
_sites = (await SiteRepository.GetAllSitesAsync()).ToList();
|
||||
_siteConnections.Clear();
|
||||
foreach (var site in _sites)
|
||||
{
|
||||
var connections = await SiteRepository.GetDataConnectionsBySiteIdAsync(site.Id);
|
||||
if (connections.Count > 0)
|
||||
{
|
||||
_siteConnections[site.Id] = connections.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
// CentralUI-012: fetch all data connections in one query and group
|
||||
// them by site, instead of issuing one query per site (N+1).
|
||||
_siteConnections = (await SiteRepository.GetAllDataConnectionsAsync())
|
||||
.GroupBy(c => c.SiteId)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -293,6 +293,13 @@
|
||||
|
||||
private string? _initError;
|
||||
|
||||
// CentralUI-009: the stream callbacks (onEvent/onTerminated) run on an
|
||||
// Akka/gRPC thread and capture `this` and `_toast`. Once the component is
|
||||
// disposed, an in-flight callback must no-op rather than touch a disposed
|
||||
// component (InvokeAsync would throw ObjectDisposedException) or a disposed
|
||||
// ToastNotification.
|
||||
private volatile bool _disposed;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
@@ -396,15 +403,18 @@
|
||||
_selectedInstanceId,
|
||||
onEvent: evt =>
|
||||
{
|
||||
// CentralUI-009: the component may have been disposed while
|
||||
// this event was in flight on the Akka/gRPC thread.
|
||||
if (_disposed) return;
|
||||
switch (evt)
|
||||
{
|
||||
case AttributeValueChanged av:
|
||||
UpsertWithCap(_attributeValues, av.AttributeName, av);
|
||||
_ = InvokeAsync(StateHasChanged);
|
||||
SafeInvokeStateHasChanged();
|
||||
break;
|
||||
case AlarmStateChanged al:
|
||||
UpsertWithCap(_alarmStates, al.AlarmName, al);
|
||||
_ = InvokeAsync(StateHasChanged);
|
||||
SafeInvokeStateHasChanged();
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -412,8 +422,11 @@
|
||||
{
|
||||
_connected = false;
|
||||
_session = null;
|
||||
_ = InvokeAsync(() =>
|
||||
// CentralUI-009: skip the toast/render if already disposed.
|
||||
if (_disposed) return;
|
||||
_ = SafeInvokeAsync(() =>
|
||||
{
|
||||
if (_disposed) return;
|
||||
_toast.ShowError("Debug stream terminated (site disconnected).");
|
||||
StateHasChanged();
|
||||
});
|
||||
@@ -546,8 +559,31 @@
|
||||
_ => "—"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Runs <paramref name="action"/> on the render thread, guarded against the
|
||||
/// component being disposed mid-flight (CentralUI-009): <c>InvokeAsync</c>
|
||||
/// throws <see cref="ObjectDisposedException"/> once the circuit is gone.
|
||||
/// </summary>
|
||||
private async Task SafeInvokeAsync(Action action)
|
||||
{
|
||||
if (_disposed) return;
|
||||
try
|
||||
{
|
||||
await InvokeAsync(action);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Component disposed between the guard and the dispatch — ignore.
|
||||
}
|
||||
}
|
||||
|
||||
private void SafeInvokeStateHasChanged() => _ = SafeInvokeAsync(StateHasChanged);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// CentralUI-009: mark disposed first so any in-flight stream callback
|
||||
// sees the flag and no-ops, then stop the stream synchronously.
|
||||
_disposed = true;
|
||||
if (_session != null)
|
||||
{
|
||||
DebugStreamService.StopStream(_session.SessionId);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@page "/monitoring/audit-log"
|
||||
@using ScadaLink.Security
|
||||
@using ScadaLink.CentralUI.Components
|
||||
@using ScadaLink.Commons.Entities.Audit
|
||||
@using ScadaLink.Commons.Interfaces.Repositories
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireAdmin)]
|
||||
@@ -195,6 +196,12 @@
|
||||
private DateTime? _filterFrom;
|
||||
private DateTime? _filterTo;
|
||||
|
||||
// The datetime-local filter inputs are in the browser's local time zone.
|
||||
// This holds new Date().getTimezoneOffset() so the values are converted to
|
||||
// UTC (CentralUI-008) rather than relabelled. Until JS interop runs it is 0
|
||||
// (UTC), which is a safe default for a UTC server/browser.
|
||||
private int _browserUtcOffsetMinutes;
|
||||
|
||||
private List<AuditLogEntry>? _entries;
|
||||
private int _totalCount;
|
||||
private int _page = 1;
|
||||
@@ -209,6 +216,23 @@
|
||||
private int TotalPages => _pageSize > 0 ? Math.Max(1, (_totalCount + _pageSize - 1) / _pageSize) : 1;
|
||||
private bool HasMore => _page * _pageSize < _totalCount;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender) return;
|
||||
try
|
||||
{
|
||||
// Date.getTimezoneOffset() returns (UTC - local) in minutes.
|
||||
_browserUtcOffsetMinutes = await JS.InvokeAsync<int>(
|
||||
"eval", "new Date().getTimezoneOffset()");
|
||||
}
|
||||
catch (Exception ex) when (ex is JSException or JSDisconnectedException
|
||||
or InvalidOperationException or TaskCanceledException)
|
||||
{
|
||||
// Prerender or a disconnected circuit: fall back to UTC (offset 0).
|
||||
_browserUtcOffsetMinutes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Search()
|
||||
{
|
||||
_page = 1;
|
||||
@@ -239,8 +263,8 @@
|
||||
user: string.IsNullOrWhiteSpace(_filterUser) ? null : _filterUser.Trim(),
|
||||
entityType: string.IsNullOrWhiteSpace(_filterEntityType) ? null : _filterEntityType.Trim(),
|
||||
action: string.IsNullOrWhiteSpace(_filterAction) ? null : _filterAction.Trim(),
|
||||
from: _filterFrom.HasValue ? new DateTimeOffset(_filterFrom.Value, TimeSpan.Zero) : null,
|
||||
to: _filterTo.HasValue ? new DateTimeOffset(_filterTo.Value, TimeSpan.Zero) : null,
|
||||
from: BrowserTime.LocalInputToUtc(_filterFrom, _browserUtcOffsetMinutes),
|
||||
to: BrowserTime.LocalInputToUtc(_filterTo, _browserUtcOffsetMinutes),
|
||||
page: _page,
|
||||
pageSize: _pageSize);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@page "/monitoring/event-logs"
|
||||
@attribute [Authorize]
|
||||
@attribute [Authorize(Policy = ScadaLink.Security.AuthorizationPolicies.RequireDeployment)]
|
||||
@using ScadaLink.Commons.Entities.Sites
|
||||
@using ScadaLink.Commons.Interfaces.Repositories
|
||||
@using ScadaLink.Commons.Messages.RemoteQuery
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@page "/monitoring/parked-messages"
|
||||
@attribute [Authorize]
|
||||
@attribute [Authorize(Policy = ScadaLink.Security.AuthorizationPolicies.RequireDeployment)]
|
||||
@using ScadaLink.Commons.Entities.Sites
|
||||
@using ScadaLink.Commons.Interfaces.Repositories
|
||||
@using ScadaLink.Commons.Messages.RemoteQuery
|
||||
|
||||
Reference in New Issue
Block a user