feat(centralui): Bundle Import filter on ConfigurationAuditLog page

This commit is contained in:
Joseph Doherty
2026-05-24 05:44:21 -04:00
parent 39f994f9bc
commit ef025a325d
4 changed files with 79 additions and 0 deletions

View File

@@ -5,6 +5,7 @@
@using ScadaLink.Commons.Interfaces.Repositories
@attribute [Authorize(Policy = AuthorizationPolicies.OperationalAudit)]
@inject ICentralUiRepository CentralUiRepository
@inject NavigationManager Nav
@inject IJSRuntime JS
<div class="container-fluid mt-3">
@@ -12,6 +13,23 @@
<ToastNotification @ref="_toast" />
@* Bundle Import filter chip (T24). Set via ?bundleImportId={guid} query
string so drill-ins from the Import wizard / other pages can scope this
page to a single import run. Cleared via the × button, which navigates
back to the page without the query param so the user sees all rows. *@
@if (BundleImportId is Guid bundleId)
{
<div class="mb-3">
<span class="badge bg-primary p-2">
Filtered by Bundle Import: <code class="text-light">@bundleId.ToString()[..8]</code>
<button type="button"
class="btn-close btn-close-white btn-sm ms-2"
aria-label="Clear Bundle Import filter"
@onclick="ClearBundleImportFilter"></button>
</span>
</div>
}
<div class="row mb-3 g-2 align-items-end">
<div class="col-md-2">
<label class="form-label small" for="audit-filter-user">User</label>
@@ -190,6 +208,13 @@
</div>
@code {
/// <summary>
/// T24 (Transport). When non-null, scopes the page to a single bundle
/// import run. Set via the <c>?bundleImportId=</c> query string from
/// drill-ins (Import wizard summary, future BundleImported row links).
/// </summary>
[SupplyParameterFromQuery, Parameter] public Guid? BundleImportId { get; set; }
private string? _filterUser;
private string? _filterEntityType;
private string? _filterAction;
@@ -216,6 +241,10 @@
private int TotalPages => _pageSize > 0 ? Math.Max(1, (_totalCount + _pageSize - 1) / _pageSize) : 1;
private bool HasMore => _page * _pageSize < _totalCount;
// Tracks the BundleImportId we last fetched against so a re-render with the
// same query param doesn't re-run the query on every parameter set.
private Guid? _lastFetchedBundleImportId;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
@@ -233,6 +262,27 @@
}
}
protected override async Task OnParametersSetAsync()
{
// T24: when the BundleImportId query param is set (or cleared), refetch
// automatically so the user lands on a pre-filtered page from a drill-in
// link without having to click Search.
if (BundleImportId != _lastFetchedBundleImportId)
{
_lastFetchedBundleImportId = BundleImportId;
_page = 1;
await FetchPage();
}
}
private void ClearBundleImportFilter()
{
// Strip the query param by navigating to the bare page route. The
// resulting OnParametersSetAsync run will refetch with BundleImportId
// back to null.
Nav.NavigateTo("/audit/configuration");
}
private async Task Search()
{
_page = 1;
@@ -265,6 +315,7 @@
action: string.IsNullOrWhiteSpace(_filterAction) ? null : _filterAction.Trim(),
from: BrowserTime.LocalInputToUtc(_filterFrom, _browserUtcOffsetMinutes),
to: BrowserTime.LocalInputToUtc(_filterTo, _browserUtcOffsetMinutes),
bundleImportId: BundleImportId,
page: _page,
pageSize: _pageSize);

View File

@@ -25,6 +25,7 @@ public interface ICentralUiRepository
DateTimeOffset? to = null,
string? entityId = null,
string? entityName = null,
Guid? bundleImportId = null,
int page = 1,
int pageSize = 50,
CancellationToken cancellationToken = default);

View File

@@ -104,6 +104,7 @@ public class CentralUiRepository : ICentralUiRepository
DateTimeOffset? to = null,
string? entityId = null,
string? entityName = null,
Guid? bundleImportId = null,
int page = 1,
int pageSize = 50,
CancellationToken cancellationToken = default)
@@ -131,6 +132,9 @@ public class CentralUiRepository : ICentralUiRepository
if (!string.IsNullOrWhiteSpace(entityName))
query = query.Where(a => a.EntityName.Contains(entityName));
if (bundleImportId is Guid bundleId)
query = query.Where(a => a.BundleImportId == bundleId);
var totalCount = await query.CountAsync(cancellationToken);
var entries = await query

View File

@@ -353,6 +353,29 @@ public class CentralUiRepositoryTests : IDisposable
Assert.Single(entries);
}
[Fact]
public async Task GetAuditLogEntries_FiltersByBundleImportId()
{
// T24 — Bundle Import filter on the Configuration Audit Log page is
// backed by the new optional bundleImportId arg on the repo query.
// Only rows stamped with the given id should come back.
var importA = Guid.NewGuid();
var importB = Guid.NewGuid();
_context.AuditLogEntries.AddRange(
new AuditLogEntry("admin", "Create", "Template", "1", "T1")
{ Timestamp = DateTimeOffset.UtcNow, BundleImportId = importA },
new AuditLogEntry("admin", "Create", "Template", "2", "T2")
{ Timestamp = DateTimeOffset.UtcNow, BundleImportId = importB },
new AuditLogEntry("admin", "Update", "Template", "3", "T3")
{ Timestamp = DateTimeOffset.UtcNow });
await _context.SaveChangesAsync();
var (entries, total) = await _repository.GetAuditLogEntriesAsync(bundleImportId: importA);
Assert.Single(entries);
Assert.Equal(1, total);
Assert.Equal(importA, entries[0].BundleImportId);
}
[Fact]
public async Task GetAuditLogEntries_ReverseChronologicalWithPagination()
{