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:
111
src/ScadaLink.CentralUI/Components/Audit/AuditResultsGrid.razor
Normal file
111
src/ScadaLink.CentralUI/Components/Audit/AuditResultsGrid.razor
Normal file
@@ -0,0 +1,111 @@
|
||||
@using ScadaLink.CentralUI.Components.Shared
|
||||
@using ScadaLink.CentralUI.Services
|
||||
@using ScadaLink.Commons.Entities.Audit
|
||||
@using ScadaLink.Commons.Types.Audit
|
||||
@using ScadaLink.Commons.Types.Enums
|
||||
@inject IAuditLogQueryService QueryService
|
||||
|
||||
<div data-test="audit-results-grid">
|
||||
@if (_error is not null)
|
||||
{
|
||||
<div class="alert alert-danger small mb-2">@_error</div>
|
||||
}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
@foreach (var col in OrderedColumns())
|
||||
{
|
||||
<th data-test="col-header-@col.Key">@col.Label</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (_rows.Count == 0)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="@OrderedColumns().Count" class="text-muted small text-center py-4">
|
||||
@if (_loading)
|
||||
{
|
||||
<span>Loading…</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>No audit events match the current filter.</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var row in _rows)
|
||||
{
|
||||
<tr @key="row.EventId"
|
||||
data-test="grid-row-@row.EventId"
|
||||
class="audit-row"
|
||||
style="cursor: pointer;"
|
||||
@onclick="() => HandleRowClick(row)">
|
||||
@foreach (var col in OrderedColumns())
|
||||
{
|
||||
<td>
|
||||
@RenderCell(col.Key, row)
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted small">Page @_pageNumber · @_rows.Count rows</span>
|
||||
<button class="btn btn-outline-secondary btn-sm"
|
||||
data-test="grid-next-page"
|
||||
disabled="@(_loading || _rows.Count < _pageSize)"
|
||||
@onclick="NextPage">Next page</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private RenderFragment RenderCell(string key, AuditEvent row) => __builder =>
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "OccurredAtUtc":
|
||||
var occurredOffset = new DateTimeOffset(DateTime.SpecifyKind(row.OccurredAtUtc, DateTimeKind.Utc));
|
||||
<span title="@row.OccurredAtUtc.ToString("u")">
|
||||
<TimestampDisplay Value="occurredOffset" Format="yyyy-MM-dd HH:mm:ss" />
|
||||
</span>
|
||||
break;
|
||||
case "Site":
|
||||
<span class="small">@(row.SourceSiteId ?? "—")</span>
|
||||
break;
|
||||
case "Channel":
|
||||
<span class="small">@row.Channel</span>
|
||||
break;
|
||||
case "Kind":
|
||||
<span class="small">@row.Kind</span>
|
||||
break;
|
||||
case "Status":
|
||||
<span data-test="status-badge-@row.EventId" class="badge @StatusBadgeClass(row.Status)">@row.Status</span>
|
||||
break;
|
||||
case "Target":
|
||||
<span class="small">@(row.Target ?? "—")</span>
|
||||
break;
|
||||
case "Actor":
|
||||
<span class="small">@(row.Actor ?? "—")</span>
|
||||
break;
|
||||
case "DurationMs":
|
||||
<span class="small font-monospace">@(row.DurationMs?.ToString() ?? "—")</span>
|
||||
break;
|
||||
case "HttpStatus":
|
||||
<span class="small font-monospace">@(row.HttpStatus?.ToString() ?? "—")</span>
|
||||
break;
|
||||
case "ErrorMessage":
|
||||
<span class="small text-danger" title="@row.ErrorMessage">@TruncateError(row.ErrorMessage)</span>
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user