feat(ui): server-side streaming CSV export of Audit Log (#23 M7)
This commit is contained in:
@@ -19,6 +19,22 @@
|
||||
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. *@
|
||||
<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>
|
||||
|
||||
@* 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. *@
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using ScadaLink.Commons.Entities.Audit;
|
||||
@@ -158,4 +159,70 @@ public partial class AuditLogPage
|
||||
// grid) shows the same row instantly without a re-render flicker.
|
||||
_drawerOpen = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bundle F (M7-T14): URL the Export-CSV link points at. Renders the most
|
||||
/// recently applied filter as query-string params so the server-side
|
||||
/// streaming endpoint reproduces the user's current view. With no filter
|
||||
/// applied yet, returns the bare endpoint — i.e. an unconstrained export.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Built here rather than in markup so the per-row test coverage can
|
||||
/// exercise the URL composition without booting the full Blazor renderer.
|
||||
/// </remarks>
|
||||
internal string ExportUrl => BuildExportUrl(_currentFilter);
|
||||
|
||||
internal static string BuildExportUrl(AuditLogQueryFilter? filter)
|
||||
{
|
||||
const string basePath = "/api/centralui/audit/export";
|
||||
if (filter is null)
|
||||
{
|
||||
return basePath;
|
||||
}
|
||||
|
||||
var parts = new List<KeyValuePair<string, string?>>(9);
|
||||
if (filter.Channel is { } ch)
|
||||
{
|
||||
parts.Add(new("channel", ch.ToString()));
|
||||
}
|
||||
if (filter.Kind is { } kind)
|
||||
{
|
||||
parts.Add(new("kind", kind.ToString()));
|
||||
}
|
||||
if (filter.Status is { } status)
|
||||
{
|
||||
parts.Add(new("status", status.ToString()));
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(filter.SourceSiteId))
|
||||
{
|
||||
parts.Add(new("site", filter.SourceSiteId));
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(filter.Target))
|
||||
{
|
||||
parts.Add(new("target", filter.Target));
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(filter.Actor))
|
||||
{
|
||||
parts.Add(new("actor", filter.Actor));
|
||||
}
|
||||
if (filter.CorrelationId is { } corr)
|
||||
{
|
||||
parts.Add(new("correlationId", corr.ToString()));
|
||||
}
|
||||
if (filter.FromUtc is { } from)
|
||||
{
|
||||
parts.Add(new("from", from.ToString("O", CultureInfo.InvariantCulture)));
|
||||
}
|
||||
if (filter.ToUtc is { } to)
|
||||
{
|
||||
parts.Add(new("to", to.ToString("O", CultureInfo.InvariantCulture)));
|
||||
}
|
||||
|
||||
if (parts.Count == 0)
|
||||
{
|
||||
return basePath;
|
||||
}
|
||||
|
||||
return QueryHelpers.AddQueryString(basePath, parts);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user