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:
@@ -8,55 +8,92 @@
|
||||
@inject CommunicationService CommunicationService
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<h4 class="mb-3">Site Event Logs</h4>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0">Site Event Logs</h4>
|
||||
<button class="btn btn-outline-secondary btn-sm"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#event-logs-filters"
|
||||
aria-expanded="true"
|
||||
aria-controls="event-logs-filters">
|
||||
Filter options (@ActiveFilterCount active)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ToastNotification @ref="_toast" />
|
||||
|
||||
<div class="row mb-3 g-2">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Site</label>
|
||||
<select class="form-select form-select-sm" @bind="_selectedSiteId">
|
||||
<option value="">Select site...</option>
|
||||
@foreach (var site in _sites)
|
||||
{
|
||||
<option value="@site.SiteIdentifier">@site.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Event Type</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_filterEventType" placeholder="e.g. ScriptError" />
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label small">Severity</label>
|
||||
<select class="form-select form-select-sm" @bind="_filterSeverity">
|
||||
<option value="">All</option>
|
||||
<option>Info</option>
|
||||
<option>Warning</option>
|
||||
<option>Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">From</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" @bind="_filterFrom" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">To</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" @bind="_filterTo" />
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label small">Keyword</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_filterKeyword" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Instance</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_filterInstanceName" placeholder="Instance name" />
|
||||
</div>
|
||||
<div class="col-md-1 d-flex align-items-end">
|
||||
<button class="btn btn-primary btn-sm" @onclick="Search" disabled="@(string.IsNullOrEmpty(_selectedSiteId) || _searching)">
|
||||
@if (_searching) { <span class="spinner-border spinner-border-sm"></span> }
|
||||
Search
|
||||
</button>
|
||||
<div class="collapse show" id="event-logs-filters">
|
||||
<div class="row mb-3 g-2 align-items-end">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small" for="filter-site">Site</label>
|
||||
<select id="filter-site" class="form-select form-select-sm" aria-label="Site" @bind="_selectedSiteId">
|
||||
<option value="">Select site...</option>
|
||||
@foreach (var site in _sites)
|
||||
{
|
||||
<option value="@site.SiteIdentifier">@site.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small" for="filter-event-type">Event Type</label>
|
||||
<input id="filter-event-type"
|
||||
type="text"
|
||||
class="form-control form-control-sm"
|
||||
aria-label="Event type"
|
||||
@bind="_filterEventType"
|
||||
placeholder="e.g. ScriptError" />
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label small" for="filter-severity">Severity</label>
|
||||
<select id="filter-severity"
|
||||
class="form-select form-select-sm"
|
||||
aria-label="Severity"
|
||||
@bind="_filterSeverity">
|
||||
<option value="">All</option>
|
||||
<option>Info</option>
|
||||
<option>Warning</option>
|
||||
<option>Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small" for="filter-from">From</label>
|
||||
<input id="filter-from"
|
||||
type="datetime-local"
|
||||
class="form-control form-control-sm"
|
||||
aria-label="From timestamp"
|
||||
@bind="_filterFrom" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small" for="filter-to">To</label>
|
||||
<input id="filter-to"
|
||||
type="datetime-local"
|
||||
class="form-control form-control-sm"
|
||||
aria-label="To timestamp"
|
||||
@bind="_filterTo" />
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label small" for="filter-keyword">Message contains</label>
|
||||
<input id="filter-keyword"
|
||||
type="text"
|
||||
class="form-control form-control-sm"
|
||||
aria-label="Message contains"
|
||||
@bind="_filterKeyword" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small" for="filter-instance">Instance</label>
|
||||
<input id="filter-instance"
|
||||
type="text"
|
||||
class="form-control form-control-sm"
|
||||
aria-label="Instance name"
|
||||
@bind="_filterInstanceName"
|
||||
placeholder="Instance name" />
|
||||
</div>
|
||||
<div class="col-md-12 d-flex gap-2">
|
||||
<button class="btn btn-primary btn-sm" @onclick="Search" disabled="@(string.IsNullOrEmpty(_selectedSiteId) || _searching)">
|
||||
@if (_searching) { <span class="spinner-border spinner-border-sm me-1" role="status"></span> }
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -70,6 +107,7 @@
|
||||
<table class="table table-sm table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th style="width: 1%;"></th>
|
||||
<th>Timestamp</th>
|
||||
<th>Type</th>
|
||||
<th>Severity</th>
|
||||
@@ -81,28 +119,59 @@
|
||||
<tbody>
|
||||
@if (_entries.Count == 0)
|
||||
{
|
||||
<tr><td colspan="6" class="text-muted text-center">No events found.</td></tr>
|
||||
<tr><td colspan="7" class="text-muted text-center">No events found.</td></tr>
|
||||
}
|
||||
@foreach (var entry in _entries)
|
||||
@for (int i = 0; i < _entries.Count; i++)
|
||||
{
|
||||
<tr class="@(entry.Severity == "Error" ? "table-danger" : entry.Severity == "Warning" ? "table-warning" : "")">
|
||||
var idx = i;
|
||||
var entry = _entries[idx];
|
||||
var rowClass = entry.Severity == "Error" ? "table-danger"
|
||||
: entry.Severity == "Warning" ? "table-warning"
|
||||
: "";
|
||||
var expanded = _expandedRows.Contains(idx);
|
||||
<tr class="@rowClass">
|
||||
<td>
|
||||
<button class="btn btn-link btn-sm p-0"
|
||||
@onclick="() => ToggleRow(idx)"
|
||||
aria-label="@(expanded ? "Hide full message" : "View full message")">
|
||||
@(expanded ? "Hide" : "View")
|
||||
</button>
|
||||
</td>
|
||||
<td class="small"><TimestampDisplay Value="@entry.Timestamp" /></td>
|
||||
<td class="small">@entry.EventType</td>
|
||||
<td><span class="badge @GetSeverityBadge(entry.Severity)">@entry.Severity</span></td>
|
||||
<td>
|
||||
<span class="badge @GetSeverityBadge(entry.Severity)" aria-label="Severity: @entry.Severity">
|
||||
@SeverityGlyph(entry.Severity) @entry.Severity
|
||||
</span>
|
||||
</td>
|
||||
<td class="small">@(entry.InstanceId ?? "—")</td>
|
||||
<td class="small">@entry.Source</td>
|
||||
<td class="small">@entry.Message</td>
|
||||
<td class="small text-truncate" style="max-width: 380px;">@entry.Message</td>
|
||||
</tr>
|
||||
@if (expanded)
|
||||
{
|
||||
<tr class="@rowClass">
|
||||
<td colspan="7">
|
||||
<pre class="small mb-0">@entry.Message</pre>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted small">@_entries.Count entries loaded</span>
|
||||
@if (_hasMore)
|
||||
{
|
||||
<button class="btn btn-outline-primary btn-sm" @onclick="LoadMore" disabled="@_searching">Load More</button>
|
||||
}
|
||||
<span class="text-muted small">Showing @_entries.Count entries</span>
|
||||
<div>
|
||||
@if (_hasMore)
|
||||
{
|
||||
<button class="btn btn-outline-primary btn-sm" @onclick="LoadMore" disabled="@_searching">Load more</button>
|
||||
}
|
||||
else if (_entries.Count > 0)
|
||||
{
|
||||
<span class="text-muted small">End of results</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -123,6 +192,23 @@
|
||||
private bool _searching;
|
||||
private string? _errorMessage;
|
||||
private ToastNotification _toast = default!;
|
||||
private readonly HashSet<int> _expandedRows = new();
|
||||
|
||||
private int ActiveFilterCount
|
||||
{
|
||||
get
|
||||
{
|
||||
var n = 0;
|
||||
if (!string.IsNullOrEmpty(_selectedSiteId)) n++;
|
||||
if (!string.IsNullOrWhiteSpace(_filterEventType)) n++;
|
||||
if (!string.IsNullOrEmpty(_filterSeverity)) n++;
|
||||
if (_filterFrom.HasValue) n++;
|
||||
if (_filterTo.HasValue) n++;
|
||||
if (!string.IsNullOrWhiteSpace(_filterKeyword)) n++;
|
||||
if (!string.IsNullOrWhiteSpace(_filterInstanceName)) n++;
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -133,11 +219,20 @@
|
||||
{
|
||||
_entries = new();
|
||||
_continuationToken = null;
|
||||
_expandedRows.Clear();
|
||||
await FetchPage();
|
||||
}
|
||||
|
||||
private async Task LoadMore() => await FetchPage();
|
||||
|
||||
private void ToggleRow(int idx)
|
||||
{
|
||||
if (!_expandedRows.Add(idx))
|
||||
{
|
||||
_expandedRows.Remove(idx);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FetchPage()
|
||||
{
|
||||
_searching = true;
|
||||
@@ -185,4 +280,12 @@
|
||||
"Info" => "bg-info text-dark",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
|
||||
private static string SeverityGlyph(string severity) => severity switch
|
||||
{
|
||||
"Error" => "⛔",
|
||||
"Warning" => "⚠",
|
||||
"Info" => "ℹ",
|
||||
_ => "•"
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user