Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/AuditResultsGrid.razor
T
Joseph Doherty 9e243493fb ui: Central UI density/consistency sweep + Theme 0.4.1
Applies the family-wide admin-UI cleanup playbook to the Central UI so the
Blazor surfaces stop diverging from the shared kit: buttons are grouped rather
than individually sized, long cell values are contained instead of widening
tables, and hard-coded colours give way to theme tokens.

The headline fix is that MainLayout passed Accent="#2f5fd0" to ThemeShell,
which the kit emits as an inline style on the shell root. Being a descendant of
<html>, it beat the [data-bs-theme="dark"] override for the entire app, so the
dark accent had never rendered. Declaring --accent in site.css :root instead
lets both schemes resolve; light is unchanged because the value already matched
the kit's light default.

Theme pins to 0.4.1, which upstreams the local .btn sizing block verbatim, so
that block is deleted here rather than duplicated. Verified byte-identical
before removal; the repo now declares no --bs-btn-* anywhere.

NOT purely cosmetic, contrary to the sweep's stated scope: four detail-modal
surfaces (NotificationReport, ConfigurationAuditLog, ParkedMessages,
SiteCallsReport) were additionally refactored from holding the selected record
to holding its id and re-resolving from the current page each render, with the
resolve doubling as the visibility gate. A background refresh that drops the
row now closes the modal instead of showing a stale snapshot. This is a
behaviour change and is called out rather than buried: a full-suite run turned
up one intermittent CentralUI failure, CloseButton_DismissesModal, whose stack
(GetRequiredEventBindingEntry during DispatchEventAsync) indicates the handler
was disposed between render and click — a window the previous field-held record
made structurally impossible. Treat the modal lifecycle here as unreviewed.

Build 0/0; suite green apart from that one intermittent failure.
2026-08-11 05:50:12 -04:00

168 lines
7.0 KiB
Plaintext

@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit
@using ZB.MOM.WW.ScadaBridge.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" @ref="_tableRef">
<thead class="table-light">
<tr>
@foreach (var col in OrderedColumns())
{
// @key keeps Blazor reusing one DOM node per column across
// re-renders (reorder/resize), so audit-grid.js binds drag
// listeners exactly once per <th> and never leaks them onto
// discarded nodes — the __auditGridCellBound guard relies on
// this node stability to be fully sound.
<th class="audit-grid-th"
@key="col.Key"
data-test="col-header-@col.Key"
data-col-key="@col.Key"
style="@ColumnWidthStyle(col.Key)">
@col.Label
<span class="audit-grid-resize-handle"
data-test="col-resize-@col.Key"
aria-hidden="true"></span>
</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 class="audit-grid-td" style="@ColumnWidthStyle(col.Key)">
@RenderCell(col.Key, row)
</td>
}
</tr>
}
}
</tbody>
</table>
</div>
@* CentralUI-032: keyset paging is naturally forward-only, but the
in-component _cursorStack lets the user step back through previous
pages by replaying the prior cursor. The Previous button is gated
on CanGoBack (the stack having at least one prior cursor — i.e. we
are not on the first page); HasNextPage mirrors the prior "short
page = end" signal (a full page suggests more rows may follow). All
cursor logic stays page-side; KeysetPager is purely presentational. *@
<KeysetPager PageNumber="_pageNumber"
RowCount="_rows.Count"
CanGoBack="CanGoBack"
HasNextPage="@(!_loading && _rows.Count >= _pageSize)"
Disabled="_loading"
OnPrevious="PrevPage"
OnNext="NextPage" />
</div>
@code {
// Compact display for Guid id columns: the first 8 hex digits, mirroring
// the drilldown drawer's ShortEventId presentation. The full value is kept
// in the cell's title attribute so it stays copy-paste accessible.
private static string ShortGuid(Guid value)
{
var n = value.ToString("N");
return n.Length >= 8 ? n[..8] : n;
}
private RenderFragment RenderCell(string key, AuditEventView 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 "Node":
<span class="small">@(row.SourceNode ?? "—")</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" title="@row.Target">@TruncateTarget(row.Target)</span>
break;
case "Actor":
<span class="small">@(row.Actor ?? "—")</span>
break;
case "ExecutionId":
@if (row.ExecutionId is { } executionId)
{
<span class="small font-monospace"
data-test="execution-id-@row.EventId"
title="@executionId">@ShortGuid(executionId)</span>
}
else
{
<span class="small text-muted">—</span>
}
break;
case "ParentExecutionId":
@if (row.ParentExecutionId is { } parentExecutionId)
{
<span class="small font-monospace"
data-test="parent-execution-id-@row.EventId"
title="@parentExecutionId">@ShortGuid(parentExecutionId)</span>
}
else
{
<span class="small text-muted">—</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;
}
};
}