112 lines
5.2 KiB
Plaintext
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" />
|