feat(ui): AuditResultsGrid + AuditLogQueryService with keyset paging (#23 M7)

Adds the results grid + query facade for the central Audit Log page
(#23 M7-T3):

* IAuditLogQueryService / AuditLogQueryService — CentralUI facade over
  IAuditLogRepository.QueryAsync so the grid can be tested with a stubbed
  query source. Default page size is 100; callers can override per call.

* AuditResultsGrid.razor + .razor.cs — Blazor Server component (Bootstrap
  only, no third-party UI libs). Renders the 10 columns from
  Component-AuditLog.md §10 (OccurredAtUtc, Site, Channel, Kind, Status,
  Target, Actor, DurationMs, HttpStatus, ErrorMessage). Keyset-paged via
  the last visible row's (OccurredAtUtc, EventId) as the cursor; Next-page
  button disabled when the current page is short (no count query). Row
  clicks emit OnRowSelected(AuditEvent) for Bundle C's drilldown drawer.
  Status badges are colour-coded (Delivered=green; Failed/Parked/Discarded
  =red; other=gray). Error messages truncated to 80 chars with full text
  on hover.

* Column model framework: a ColumnOrder [Parameter] reorders columns by
  stable string keys; unknown keys are dropped. M7 scope decision (in the
  class doc): the framework is in place but drag-reorder / resize UX is
  not implemented — M7.x can add persisted-per-user reordering without
  rewriting the column model.

* AuditLogPage wired: hosts AuditFilterBar + AuditResultsGrid, threads
  the filter through and stubs OnRowSelected for Bundle C.

* AuditLogQueryService registered as scoped in AddCentralUI.

* Tests: 4 grid bUnit tests (10 columns rendered, next-page cursor
  carries last row, row click raises callback, badge classes for
  Failed vs Delivered), 2 service tests (filter+paging pass-through,
  default page size of 100). AuditLogPageScaffoldTests updated to
  provide the new ISiteRepository + IAuditLogQueryService stubs the
  page now resolves.
This commit is contained in:
Joseph Doherty
2026-05-20 20:02:46 -04:00
parent 13e84a76a7
commit e052aa4ff8
10 changed files with 611 additions and 7 deletions
@@ -0,0 +1,30 @@
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Types.Audit;
namespace ScadaLink.CentralUI.Services;
/// <summary>
/// CentralUI facade over <see cref="ScadaLink.Commons.Interfaces.Repositories.IAuditLogRepository"/>
/// (#23 M7-T3). The Audit Log page's results grid talks to this service rather than
/// the repository directly so tests can substitute a fake without spinning up EF
/// Core, and so a future caching / shaping layer (e.g. server-side CSV streaming)
/// can hang off the same seam.
/// </summary>
public interface IAuditLogQueryService
{
/// <summary>
/// Returns a keyset-paged result page for <paramref name="filter"/>. When
/// <paramref name="paging"/> is <c>null</c>, defaults to <see cref="DefaultPageSize"/>
/// rows with no cursor (first page). The repository orders by
/// <c>(OccurredAtUtc DESC, EventId DESC)</c>; pass the last row's
/// <see cref="AuditEvent.OccurredAtUtc"/> + <see cref="AuditEvent.EventId"/>
/// back as the cursor for the next page.
/// </summary>
Task<IReadOnlyList<AuditEvent>> QueryAsync(
AuditLogQueryFilter filter,
AuditLogPaging? paging = null,
CancellationToken ct = default);
/// <summary>Default page size when callers don't specify one.</summary>
int DefaultPageSize { get; }
}