refactor(ui/monitoring): KPI dashboard, message expand, copy, pagination fix
Dashboard: user-info card demoted; 4 KPI cards (Sites, Data
connections, Templates, API keys) sourced from existing repositories;
3 Quick-action link cards (Health, Audit Log, Templates). Inline
max-width style replaced with Bootstrap utilities.
Health: KPI row condensed to Online / Offline / Sites with active
errors (Total Sites and Total Script Errors dropped). Per-site cards
re-laid out 2-column with each subsection (Data Connections,
Instances & Queues, Errors & Parked Messages) inside Bootstrap
collapse panels collapsed by default. Online / Offline / Primary /
Standby badges paired with shape glyphs (o / * / triangle) plus
aria-label.
EventLogs: filter row wrapped in a Bootstrap collapse toggled by
"Filter options (n active)"; per-row View toggle reveals the full
message in a collapse row; "Keyword" relabeled "Message contains";
all filter inputs gain id+label-for+aria-label; severity badges paired
with a leading glyph; explicit "End of results" terminator on
Load more.
ParkedMessages: Message ID rendered as <code>{first 12}...</code>
plus a clipboard button; per-row View toggle reveals full error;
action buttons get aria-label="{Retry|Discard} message {id}";
in-flight spinner inside the active button.
AuditLog: pagination Next-disabled now uses
_page * _pageSize >= _totalCount via HasMore helper (fixes the
exactly-page-size edge case). Clear filters button added. Entity ID
rendered as code + clipboard button. View/Hide buttons gain
aria-label referencing the entry id. State JSON larger than 1 KB
renders a "View in modal" button instead of the inline overflow.
This commit is contained in:
@@ -6,17 +6,18 @@
|
||||
@using ScadaLink.Communication
|
||||
@inject ISiteRepository SiteRepository
|
||||
@inject CommunicationService CommunicationService
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<h4 class="mb-3">Parked Messages</h4>
|
||||
|
||||
<ToastNotification @ref="_toast" />
|
||||
<ConfirmDialog @ref="_confirmDialog" />
|
||||
<ConfirmDialog @ref="_confirmDialog" ConfirmButtonClass="btn-danger" />
|
||||
|
||||
<div class="row mb-3 g-2">
|
||||
<div class="row mb-3 g-2 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Site</label>
|
||||
<select class="form-select form-select-sm" @bind="_selectedSiteId">
|
||||
<label class="form-label small" for="pm-filter-site">Site</label>
|
||||
<select id="pm-filter-site" class="form-select form-select-sm" aria-label="Site" @bind="_selectedSiteId">
|
||||
<option value="">Select site...</option>
|
||||
@foreach (var site in _sites)
|
||||
{
|
||||
@@ -24,10 +25,10 @@
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-primary btn-sm" @onclick="Search"
|
||||
disabled="@(string.IsNullOrEmpty(_selectedSiteId) || _searching)">
|
||||
@if (_searching) { <span class="spinner-border spinner-border-sm"></span> }
|
||||
@if (_searching) { <span class="spinner-border spinner-border-sm me-1" role="status"></span> }
|
||||
Query
|
||||
</button>
|
||||
</div>
|
||||
@@ -43,6 +44,7 @@
|
||||
<table class="table table-sm table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th style="width: 1%;"></th>
|
||||
<th>Message ID</th>
|
||||
<th>Target System</th>
|
||||
<th>Method</th>
|
||||
@@ -56,27 +58,70 @@
|
||||
<tbody>
|
||||
@if (_messages.Count == 0)
|
||||
{
|
||||
<tr><td colspan="8" class="text-muted text-center">No parked messages.</td></tr>
|
||||
<tr><td colspan="9" class="text-muted text-center">No parked messages.</td></tr>
|
||||
}
|
||||
@foreach (var msg in _messages)
|
||||
@for (int i = 0; i < _messages.Count; i++)
|
||||
{
|
||||
var idx = i;
|
||||
var msg = _messages[idx];
|
||||
var idShort = msg.MessageId[..Math.Min(12, msg.MessageId.Length)];
|
||||
var expanded = _expandedRows.Contains(idx);
|
||||
var retryActive = _actionInProgress && _activeMessageId == msg.MessageId && _activeAction == "Retry";
|
||||
var discardActive = _actionInProgress && _activeMessageId == msg.MessageId && _activeAction == "Discard";
|
||||
<tr>
|
||||
<td class="small"><code>@msg.MessageId[..Math.Min(12, msg.MessageId.Length)]</code></td>
|
||||
<td>
|
||||
<button class="btn btn-link btn-sm p-0"
|
||||
@onclick="() => ToggleRow(idx)"
|
||||
aria-label="@(expanded ? "Hide error details" : "View error details")">
|
||||
@(expanded ? "Hide" : "View")
|
||||
</button>
|
||||
</td>
|
||||
<td class="small">
|
||||
<code class="small">@idShort…</code>
|
||||
<button class="btn btn-link btn-sm p-0 ms-1"
|
||||
@onclick="() => CopyAsync(msg.MessageId)"
|
||||
title="Copy message ID"
|
||||
aria-label="Copy message ID @msg.MessageId">📋</button>
|
||||
</td>
|
||||
<td class="small">@msg.TargetSystem</td>
|
||||
<td class="small">@msg.MethodName</td>
|
||||
<td class="small text-danger">@msg.ErrorMessage</td>
|
||||
<td class="small text-danger text-truncate" style="max-width: 320px;">@msg.ErrorMessage</td>
|
||||
<td class="small text-center">@msg.AttemptCount</td>
|
||||
<td class="small"><TimestampDisplay Value="@msg.OriginalTimestamp" /></td>
|
||||
<td class="small"><TimestampDisplay Value="@msg.LastAttemptTimestamp" /></td>
|
||||
<td>
|
||||
<button class="btn btn-outline-success btn-sm py-0 px-1 me-1"
|
||||
@onclick="() => RetryMessage(msg)" disabled="@_actionInProgress"
|
||||
title="Retry message (move back to pending)">Retry</button>
|
||||
@onclick="() => RetryMessage(msg)"
|
||||
disabled="@_actionInProgress"
|
||||
title="Retry message (move back to pending)"
|
||||
aria-label="Retry message @idShort">
|
||||
@if (retryActive)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
|
||||
}
|
||||
Retry
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-1"
|
||||
@onclick="() => DiscardMessage(msg)" disabled="@_actionInProgress"
|
||||
title="Permanently discard message">Discard</button>
|
||||
@onclick="() => DiscardMessage(msg)"
|
||||
disabled="@_actionInProgress"
|
||||
title="Permanently discard message"
|
||||
aria-label="Discard message @idShort">
|
||||
@if (discardActive)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
|
||||
}
|
||||
Discard
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@if (expanded)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="9">
|
||||
<pre class="small mb-0">@msg.ErrorMessage</pre>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -105,8 +150,11 @@
|
||||
private string? _errorMessage;
|
||||
|
||||
private bool _actionInProgress;
|
||||
private string? _activeMessageId;
|
||||
private string? _activeAction;
|
||||
private ToastNotification _toast = default!;
|
||||
private ConfirmDialog _confirmDialog = default!;
|
||||
private readonly HashSet<int> _expandedRows = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -116,12 +164,34 @@
|
||||
private async Task Search()
|
||||
{
|
||||
_pageNumber = 1;
|
||||
_expandedRows.Clear();
|
||||
await FetchPage();
|
||||
}
|
||||
|
||||
private async Task PrevPage() { _pageNumber--; await FetchPage(); }
|
||||
private async Task NextPage() { _pageNumber++; await FetchPage(); }
|
||||
|
||||
private void ToggleRow(int idx)
|
||||
{
|
||||
if (!_expandedRows.Add(idx))
|
||||
{
|
||||
_expandedRows.Remove(idx);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CopyAsync(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("navigator.clipboard.writeText", text);
|
||||
_toast.ShowSuccess("Copied to clipboard.");
|
||||
}
|
||||
catch
|
||||
{
|
||||
_toast.ShowError("Copy failed.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FetchPage()
|
||||
{
|
||||
_searching = true;
|
||||
@@ -141,6 +211,7 @@
|
||||
{
|
||||
_messages = response.Messages.ToList();
|
||||
_totalCount = response.TotalCount;
|
||||
_expandedRows.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -157,6 +228,8 @@
|
||||
private async Task RetryMessage(ParkedMessageEntry msg)
|
||||
{
|
||||
_actionInProgress = true;
|
||||
_activeMessageId = msg.MessageId;
|
||||
_activeAction = "Retry";
|
||||
try
|
||||
{
|
||||
var request = new ParkedMessageRetryRequest(
|
||||
@@ -179,6 +252,8 @@
|
||||
{
|
||||
_toast.ShowError($"Retry failed: {ex.Message}");
|
||||
}
|
||||
_activeMessageId = null;
|
||||
_activeAction = null;
|
||||
_actionInProgress = false;
|
||||
}
|
||||
|
||||
@@ -190,6 +265,8 @@
|
||||
if (!confirmed) return;
|
||||
|
||||
_actionInProgress = true;
|
||||
_activeMessageId = msg.MessageId;
|
||||
_activeAction = "Discard";
|
||||
try
|
||||
{
|
||||
var request = new ParkedMessageDiscardRequest(
|
||||
@@ -212,6 +289,8 @@
|
||||
{
|
||||
_toast.ShowError($"Discard failed: {ex.Message}");
|
||||
}
|
||||
_activeMessageId = null;
|
||||
_activeAction = null;
|
||||
_actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user