Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Audit/AuditLogPage.razor
T

112 lines
5.2 KiB
Plaintext

@page "/audit/log"
@attribute [Authorize(Policy = AuthorizationPolicies.OperationalAudit)]
@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit
@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.Security
@inject IAuditLogQueryService AuditLogQueryService
<PageTitle>Audit Log</PageTitle>
<div class="container-fluid mt-3">
<h1 class="h4 mb-3">Audit Log</h1>
@* Trends panel (M6 / K15). A best-effort collapsible Bootstrap card sitting
above the audit query UI: one KpiTrendChart per AuditLog global metric over
a 24h (default) / 7d window. Series are fetched independently in the
code-behind — a failed fetch degrades only that chart to the unavailable
placeholder and never breaks the filter bar / grid below. *@
<div class="card mb-3" data-test="audit-trends">
<div class="card-header d-flex justify-content-between align-items-center">
<button type="button"
class="btn btn-link p-0 text-decoration-none fw-semibold"
aria-expanded="@_trendsOpen"
@onclick="ToggleTrendsAsync">
Trends
<span class="text-muted small">@(_trendsOpen ? "▾" : "▸")</span>
</button>
@if (_trendsOpen)
{
<div class="btn-group btn-group-sm" role="group" aria-label="Trend window">
<button type="button"
class="btn @(_windowHours == 24 ? "btn-secondary" : "btn-outline-secondary")"
@onclick="() => SetWindowAsync(24)" disabled="@_trendsLoading">
24h
</button>
<button type="button"
class="btn @(_windowHours == 168 ? "btn-secondary" : "btn-outline-secondary")"
@onclick="() => SetWindowAsync(168)" disabled="@_trendsLoading">
7d
</button>
</div>
}
</div>
@if (_trendsOpen)
{
<div class="card-body">
<div class="row g-3">
@foreach (var (metric, title, unit) in TrendMetrics)
{
var series = SeriesFor(metric);
<div class="col-12 col-md-4">
<KpiTrendChart Points="@series.Points"
Title="@title"
Unit="@unit"
IsAvailable="@series.IsAvailable"
ErrorMessage="@series.ErrorMessage" />
</div>
}
</div>
</div>
}
</div>
@* 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"
InitialInstanceSearch="@_initialInstanceSearch" />
</div>
@* Export button (Bundle F / M7-T14). A plain <a download> link triggers the
streaming CSV endpoint at /api/centralui/audit/export — chosen over a
SignalR-driven download because the request can stream 100k rows directly
to the response body without buffering through the Blazor circuit. The
href reflects the most recently applied filter; before Apply is clicked,
an unconstrained export is exposed.
Bundle G (#23 M7-T15) gates the button on the AuditExport policy so an
OperationalAudit-only operator (read access without bulk export) sees the
page + filters but cannot trigger the CSV pull. The endpoint itself is
gated separately, so a hand-crafted URL still 403s — the AuthorizeView
here is the user-facing affordance, not the authoritative check. *@
<AuthorizeView Policy="@AuthorizationPolicies.AuditExport">
<Authorized Context="exportContext">
<div class="mb-3 d-flex justify-content-end">
<a class="btn btn-outline-secondary btn-sm"
href="@ExportUrl"
download
role="button"
aria-label="Export current view to CSV">
Export CSV
</a>
</div>
</Authorized>
</AuthorizeView>
@* Results grid (Bundle B / M7-T3). Row clicks emit OnRowSelected for Bundle C's
drilldown drawer; the grid stays in "no events" mode until the user applies a
filter so the page does not auto-load the full audit table on first render. *@
<div>
<AuditResultsGrid Filter="@_currentFilter" OnRowSelected="HandleRowSelected" />
</div>
</div>
@* Drilldown drawer (Bundle C / M7-T4..T8). Hosted at the page level so the
off-canvas overlay sits above the grid / filter bar irrespective of scroll. *@
<AuditDrilldownDrawer Event="@_selectedEvent"
IsOpen="@_drawerOpen"
OnClose="HandleDrawerClose" />