feat(ui): AuditLogPage parses query-string filters for drill-ins (#23 M7)
This commit is contained in:
@@ -32,8 +32,26 @@ public partial class AuditFilterBar
|
||||
/// </summary>
|
||||
[Parameter] public Func<DateTime>? NowUtcProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bundle D drill-in seam (#23 M7-T10..T12). When set on first render,
|
||||
/// pre-populates the Instance free-text input. Instance is UI-only — the
|
||||
/// repository filter contract has no instance column — so this flows in
|
||||
/// through a separate parameter rather than the <see cref="AuditLogQueryFilter"/>
|
||||
/// the parent page passes to the grid.
|
||||
/// </summary>
|
||||
[Parameter] public string? InitialInstanceSearch { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// One-shot prefill from a drill-in deep link. Subsequent parameter changes
|
||||
// do NOT overwrite user input — the field is owned by the operator after
|
||||
// first render.
|
||||
if (!string.IsNullOrWhiteSpace(InitialInstanceSearch))
|
||||
{
|
||||
_model.InstanceSearch = InitialInstanceSearch.Trim();
|
||||
}
|
||||
|
||||
|
||||
// Populate the Site chips at component init. Failure is non-fatal — the chip
|
||||
// section just shows "No sites available." Sites are listed by Name to match
|
||||
// operator expectations from the Notification Report.
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
<div class="container-fluid mt-3">
|
||||
<h1 class="h4 mb-3">Audit Log</h1>
|
||||
|
||||
@* Filter bar (Bundle B / M7-T2). Apply hands the collapsed filter to the grid. *@
|
||||
@* Filter bar (Bundle B / M7-T2). Apply hands the collapsed filter to the grid.
|
||||
Bundle D (M7-T10..T12) threads a query-string instance prefill through
|
||||
InitialInstanceSearch — UI-only because the filter contract has no instance column. *@
|
||||
<div class="mb-3">
|
||||
<AuditFilterBar OnFilterChanged="HandleFilterChanged" />
|
||||
<AuditFilterBar OnFilterChanged="HandleFilterChanged"
|
||||
InitialInstanceSearch="@_initialInstanceSearch" />
|
||||
</div>
|
||||
|
||||
@* Results grid (Bundle B / M7-T3). Row clicks emit OnRowSelected for Bundle C's
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using ScadaLink.Commons.Entities.Audit;
|
||||
using ScadaLink.Commons.Types.Audit;
|
||||
using ScadaLink.Commons.Types.Enums;
|
||||
|
||||
namespace ScadaLink.CentralUI.Components.Pages.Audit;
|
||||
|
||||
@@ -11,12 +14,113 @@ namespace ScadaLink.CentralUI.Components.Pages.Audit;
|
||||
/// trigger). Row clicks land in <see cref="HandleRowSelected"/> — Bundle C wires
|
||||
/// this to the drilldown drawer; for now it is a no-op seam so test stubs do
|
||||
/// not error.
|
||||
///
|
||||
/// <para>
|
||||
/// Bundle D (M7-T10..T12) adds query-string drill-in parsing so other pages can
|
||||
/// deep-link to a pre-filtered Audit Log: <c>?correlationId=</c>, <c>?target=</c>,
|
||||
/// <c>?actor=</c>, <c>?site=</c>, <c>?channel=</c>, and the UI-only
|
||||
/// <c>?instance=</c> are read on initialization. When any param is present we
|
||||
/// allocate a fresh <see cref="AuditLogQueryFilter"/> and assign it to
|
||||
/// <see cref="_currentFilter"/>, which kicks the results grid into auto-load
|
||||
/// without the user clicking Apply. Unknown values (e.g. an invalid enum name)
|
||||
/// are silently dropped — the page still renders, just without that constraint.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public partial class AuditLogPage
|
||||
{
|
||||
[Inject] private NavigationManager Navigation { get; set; } = null!;
|
||||
|
||||
private AuditLogQueryFilter? _currentFilter;
|
||||
private AuditEvent? _selectedEvent;
|
||||
private bool _drawerOpen;
|
||||
private string? _initialInstanceSearch;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
ApplyQueryStringFilters();
|
||||
}
|
||||
|
||||
private void ApplyQueryStringFilters()
|
||||
{
|
||||
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
|
||||
if (query.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Guid? correlationId = null;
|
||||
if (query.TryGetValue("correlationId", out var corrValues)
|
||||
&& Guid.TryParse(corrValues.ToString(), out var parsedCorr))
|
||||
{
|
||||
correlationId = parsedCorr;
|
||||
}
|
||||
|
||||
string? target = null;
|
||||
if (query.TryGetValue("target", out var targetValues))
|
||||
{
|
||||
var v = targetValues.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(v))
|
||||
{
|
||||
target = v.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
string? actor = null;
|
||||
if (query.TryGetValue("actor", out var actorValues))
|
||||
{
|
||||
var v = actorValues.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(v))
|
||||
{
|
||||
actor = v.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
string? site = null;
|
||||
if (query.TryGetValue("site", out var siteValues))
|
||||
{
|
||||
var v = siteValues.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(v))
|
||||
{
|
||||
site = v.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
AuditChannel? channel = null;
|
||||
if (query.TryGetValue("channel", out var channelValues)
|
||||
&& Enum.TryParse<AuditChannel>(channelValues.ToString(), ignoreCase: true, out var parsedChannel))
|
||||
{
|
||||
channel = parsedChannel;
|
||||
}
|
||||
|
||||
// Instance is UI-only — the filter contract has no matching column, so we
|
||||
// pass it as a separate seam to the filter bar.
|
||||
if (query.TryGetValue("instance", out var instanceValues))
|
||||
{
|
||||
var v = instanceValues.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(v))
|
||||
{
|
||||
_initialInstanceSearch = v.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
// If ANY filter-shaped param was provided, allocate the filter so the grid
|
||||
// auto-loads. Pure ?instance= deep links (UI-only) do not trigger auto-load
|
||||
// because the filter contract has no instance column — the user still needs
|
||||
// to refine + Apply for those.
|
||||
if (correlationId is null && target is null && actor is null && site is null && channel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_currentFilter = new AuditLogQueryFilter(
|
||||
Channel: channel,
|
||||
SourceSiteId: site,
|
||||
Target: target,
|
||||
Actor: actor,
|
||||
CorrelationId: correlationId);
|
||||
}
|
||||
|
||||
private void HandleFilterChanged(AuditLogQueryFilter filter)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user