using System.Globalization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using ScadaLink.CentralUI.Services;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Types.Audit;
using ScadaLink.Commons.Types.Enums;
namespace ScadaLink.CentralUI.Components.Audit;
///
/// Execution-Tree Node Detail Modal (Execution-Tree Node Detail Modal feature,
/// Task 3). Opened from an execution-tree node double-click: given an
/// it loads that execution's audit rows via
/// and shows a list → per-row detail.
///
///
/// Chrome. A hand-rolled Bootstrap modal — visibility is pure Blazor
/// state () plus the d-block/show CSS classes
/// and a sibling modal-backdrop, mirroring how
/// hand-rolls its offcanvas. No
/// bootstrap.bundle.js modal API is used.
///
///
///
/// Load timing. The modal queries only on the closed → open transition
/// (detected in ), never on every parameter
/// change, so re-renders while open do not re-hit the service.
///
///
///
/// States. Two-or-more rows → list view (one button per row, click sets
/// the selected row); exactly one row → opens straight to the detail view;
/// zero rows → a friendly empty state. A query failure degrades to an inline
/// error banner — it is never rethrown, so a transient DB outage cannot kill
/// the SignalR circuit (the same posture as ExecutionTreePage.LoadChainAsync).
/// The per-row detail body is delegated to the shared .
///
///
public partial class ExecutionDetailModal
{
[Inject] private IAuditLogQueryService AuditLogQueryService { get; set; } = null!;
///
/// The execution whose audit rows the modal loads. When null an open modal
/// loads nothing and shows the empty state — the host is expected to pair a
/// non-null id with .
///
[Parameter] public Guid? ExecutionId { get; set; }
///
/// True when the host wants the modal visible. The closed → open transition
/// triggers the row load; see .
///
[Parameter] public bool IsOpen { get; set; }
///
/// Fired when the user dismisses the modal (header X, backdrop click, or
/// footer Close). The host is expected to flip to false.
///
[Parameter] public EventCallback OnClose { get; set; }
// The loaded rows for the current execution; empty until a load completes.
private IReadOnlyList _rows = Array.Empty();
// The row whose detail is shown; null = list view.
private AuditEvent? _selectedRow;
private bool _loading;
private string? _error;
// Tracks the previous IsOpen so OnParametersSet can detect the open
// transition and load exactly once per open, not on every parameter change.
private bool _wasOpen;
///
/// Page size for the execution-row query. One execution's audit rows are
/// few (cached calls top out around 4–5 rows); 100 comfortably covers a
/// whole execution without paging.
///
private const int RowPageSize = 100;
protected override async Task OnParametersSetAsync()
{
// Load only on the closed → open transition. A re-render while already
// open (or while closed) must not re-hit the service.
if (IsOpen && !_wasOpen)
{
await LoadRowsAsync();
}
_wasOpen = IsOpen;
}
///
/// Loads the current execution's audit rows. On success, a single-row
/// result opens straight to the detail view; otherwise the list view shows.
/// A query failure degrades to an inline error banner and is never
/// rethrown — audit drill-in is best-effort and must not kill the circuit.
///
private async Task LoadRowsAsync()
{
_loading = true;
_error = null;
_selectedRow = null;
_rows = Array.Empty();
if (ExecutionId is null)
{
// Nothing to load — fall through to the empty state.
_loading = false;
return;
}
try
{
// No CancellationToken is passed deliberately: this is a bounded,
// small (~100-row) query for one execution, so the IDisposable/CTS
// machinery is not worth it for a modal. The closed → open guard in
// OnParametersSetAsync cleanly re-loads on the next open if needed.
_rows = await AuditLogQueryService.QueryAsync(
new AuditLogQueryFilter(ExecutionId: ExecutionId.Value),
new AuditLogPaging(PageSize: RowPageSize));
// A single-row execution opens straight to its detail — there is
// no list to choose from.
if (_rows.Count == 1)
{
_selectedRow = _rows[0];
}
}
catch (Exception ex)
{
// Mirror ExecutionTreePage.LoadChainAsync: a transient DB outage
// degrades the modal to an inline error banner rather than killing
// the SignalR circuit. Never rethrow.
_error = $"Could not load this execution's audit rows: {ex.Message}";
_rows = Array.Empty();
_selectedRow = null;
}
finally
{
_loading = false;
}
}
private void SelectRow(AuditEvent row) => _selectedRow = row;
private void BackToList() => _selectedRow = null;
private async Task HandleClose()
{
if (OnClose.HasDelegate)
{
await OnClose.InvokeAsync();
}
}
///
/// Closes the modal when Escape is pressed, matching the header X, backdrop
/// click, and footer Close affordances. The root .modal div carries
/// tabindex="-1" so it can receive the keydown.
///
private async Task HandleKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Escape")
{
await HandleClose();
}
}
/// First 8 hex digits of the execution id, mirroring the UI's short-id convention.
private string ShortExecutionId()
{
if (ExecutionId is null)
{
return "—";
}
var n = ExecutionId.Value.ToString("N");
return n.Length >= 8 ? n[..8] : n;
}
private static string FormatTime(DateTime occurredAtUtc)
=> occurredAtUtc.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture);
///
/// Bootstrap badge class for a row's status — green for the success
/// terminal state, red for failure/discard, amber for in-flight. Mirrors
/// the status-badge colouring used by the Audit Log results grid.
///
private static string StatusBadgeClass(AuditStatus status) => status switch
{
AuditStatus.Delivered => "text-bg-success",
AuditStatus.Failed or AuditStatus.Discarded or AuditStatus.Parked => "text-bg-danger",
_ => "text-bg-warning",
};
}