refactor(ui/deployment): live-updates toggle, DebugView guardrails
New shared DiffDialog mirroring ConfirmDialog's API
(ShowAsync(title, before, after)) so live-data pages stop
hand-rolling Bootstrap modal markup.
Topology: <h4> in flex header, aria-labels on Expand/Collapse/Refresh
and the inline rename input, Live-updates toggle (suppresses the 15s
timer when off), instance/area counts moved into a summary alert
above the tree, Stale badge paired with bi-exclamation-triangle icon
+ aria-label, hand-rolled Diff modal replaced with <DiffDialog @ref>.
Deployments: pause/resume auto-refresh button replaces the static
"Auto-refresh: 10s" text; summary cards switch to
col-lg-3 col-md-6 col-12; InProgress spinner gets role="status" +
aria-label; failed rows pick up a bi-x-circle icon next to the
Status badge; Deployment ID + Revision folded into one
{id}@{revision[..8]} cell; inline Error column collapses behind a
per-row "View error" toggle; bare empty-state text upgraded to the
centered muted block.
DebugView: status-strip card at the top showing instance / connection
state / last snapshot timestamp plus a "Start fresh" button when the
page auto-reconnected from localStorage. Per-table filter input,
scroll-lock toggle, Clear button, and a 200-row queue-style cap.
<tbody> elements gain aria-live="polite" aria-atomic="false" for
screen-reader announcements. Quality and Alarm-State badges get
aria-labels; timestamps display HH:mm:ss with full ms in a hover
tooltip. Auto-reconnect surfaces a toast with autoDismissMs: 8000.
This commit is contained in:
@@ -12,9 +12,12 @@
|
||||
<div class="container-fluid mt-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0">Deployment Status</h4>
|
||||
<div>
|
||||
<span class="text-muted small me-2">Auto-refresh: 10s</span>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="LoadDataAsync">Refresh</button>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="ToggleAutoRefresh"
|
||||
aria-label="@(_autoRefresh ? "Pause auto-refresh" : "Resume auto-refresh")">
|
||||
@(_autoRefresh ? "⏸ Pause updates" : "▶ Resume updates")
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="LoadDataAsync" aria-label="Refresh deployments">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +33,7 @@
|
||||
{
|
||||
@* Summary cards *@
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="col-lg-3 col-md-6 col-12">
|
||||
<div class="card border-warning">
|
||||
<div class="card-body text-center py-2">
|
||||
<h4 class="mb-0 text-warning">@_records.Count(r => r.Status == DeploymentStatus.Pending)</h4>
|
||||
@@ -38,7 +41,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-lg-3 col-md-6 col-12">
|
||||
<div class="card border-info">
|
||||
<div class="card-body text-center py-2">
|
||||
<h4 class="mb-0 text-info">@_records.Count(r => r.Status == DeploymentStatus.InProgress)</h4>
|
||||
@@ -46,7 +49,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-lg-3 col-md-6 col-12">
|
||||
<div class="card border-success">
|
||||
<div class="card-body text-center py-2">
|
||||
<h4 class="mb-0 text-success">@_records.Count(r => r.Status == DeploymentStatus.Success)</h4>
|
||||
@@ -54,7 +57,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-lg-3 col-md-6 col-12">
|
||||
<div class="card border-danger">
|
||||
<div class="card-body text-center py-2">
|
||||
<h4 class="mb-0 text-danger">@_records.Count(r => r.Status == DeploymentStatus.Failed)</h4>
|
||||
@@ -64,37 +67,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-sm table-striped table-hover">
|
||||
@if (_records.Count == 0)
|
||||
{
|
||||
<div class="text-center py-5 text-muted">
|
||||
<p class="mb-0">No deployments recorded.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm table-striped table-hover align-middle">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Deployment ID</th>
|
||||
<th>Deployment</th>
|
||||
<th>Instance</th>
|
||||
<th>Status</th>
|
||||
<th>Deployed By</th>
|
||||
<th>Started</th>
|
||||
<th>Completed</th>
|
||||
<th>Revision</th>
|
||||
<th>Error</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (_records.Count == 0)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="8" class="text-muted text-center">No deployments recorded.</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var record in _pagedRecords)
|
||||
{
|
||||
<tr class="@GetRowClass(record.Status)">
|
||||
<td><code class="small">@record.DeploymentId[..Math.Min(12, record.DeploymentId.Length)]...</code></td>
|
||||
var rowId = $"deploy-row-{record.DeploymentId}";
|
||||
var errorCollapseId = $"deploy-err-{record.DeploymentId}";
|
||||
var isFailed = record.Status == DeploymentStatus.Failed;
|
||||
var idShort = record.DeploymentId[..Math.Min(12, record.DeploymentId.Length)];
|
||||
var revShort = record.RevisionHash?[..Math.Min(8, record.RevisionHash?.Length ?? 0)];
|
||||
<tr id="@rowId" class="@GetRowClass(record.Status)">
|
||||
<td>
|
||||
<code class="small">@idShort@(string.IsNullOrEmpty(revShort) ? "" : $"@{revShort}")</code>
|
||||
</td>
|
||||
<td>@GetInstanceName(record.InstanceId)</td>
|
||||
<td>
|
||||
<span class="badge @GetStatusBadge(record.Status)">
|
||||
@if (isFailed)
|
||||
{
|
||||
<i class="bi bi-x-circle text-danger me-1" aria-hidden="true"></i>
|
||||
}
|
||||
<span class="badge @GetStatusBadge(record.Status)"
|
||||
aria-label="@($"Deployment status: {record.Status}")">
|
||||
@record.Status
|
||||
@if (record.Status == DeploymentStatus.InProgress)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm ms-1" style="width: 0.7rem; height: 0.7rem;"></span>
|
||||
<span class="spinner-border spinner-border-sm ms-1" style="width: 0.7rem; height: 0.7rem;"
|
||||
role="status" aria-label="Deployment in progress"></span>
|
||||
}
|
||||
</span>
|
||||
</td>
|
||||
@@ -112,12 +129,33 @@
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td class="small"><code>@(record.RevisionHash?[..Math.Min(8, record.RevisionHash?.Length ?? 0)])</code></td>
|
||||
<td class="small text-danger">@(record.ErrorMessage ?? "")</td>
|
||||
<td class="small text-end">
|
||||
@if (isFailed && !string.IsNullOrEmpty(record.ErrorMessage))
|
||||
{
|
||||
<button class="btn btn-link btn-sm p-0" type="button"
|
||||
@onclick="() => ToggleErrorExpansion(record.DeploymentId)"
|
||||
aria-expanded="@(IsErrorExpanded(record.DeploymentId) ? "true" : "false")"
|
||||
aria-controls="@errorCollapseId">
|
||||
@(IsErrorExpanded(record.DeploymentId) ? "Hide error" : "View error")
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
@if (isFailed && !string.IsNullOrEmpty(record.ErrorMessage) && IsErrorExpanded(record.DeploymentId))
|
||||
{
|
||||
<tr id="@errorCollapseId" class="table-danger">
|
||||
<td colspan="7">
|
||||
<div class="small">
|
||||
<strong>Error:</strong>
|
||||
<pre class="mb-0 mt-1 small" style="white-space: pre-wrap; word-break: break-word;">@record.ErrorMessage</pre>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@if (_totalPages > 1)
|
||||
{
|
||||
@@ -149,22 +187,56 @@
|
||||
private bool _loading = true;
|
||||
private string? _errorMessage;
|
||||
private Timer? _refreshTimer;
|
||||
private bool _autoRefresh = true;
|
||||
private readonly HashSet<string> _expandedErrors = new();
|
||||
|
||||
private int _currentPage = 1;
|
||||
private int _totalPages;
|
||||
private const int PageSize = 25;
|
||||
private static readonly TimeSpan RefreshInterval = TimeSpan.FromSeconds(10);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadDataAsync();
|
||||
StartTimer();
|
||||
}
|
||||
|
||||
private void StartTimer()
|
||||
{
|
||||
_refreshTimer?.Dispose();
|
||||
_refreshTimer = new Timer(_ =>
|
||||
{
|
||||
InvokeAsync(async () =>
|
||||
{
|
||||
if (!_autoRefresh) return;
|
||||
await LoadDataAsync();
|
||||
StateHasChanged();
|
||||
});
|
||||
}, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10));
|
||||
}, null, RefreshInterval, RefreshInterval);
|
||||
}
|
||||
|
||||
private void ToggleAutoRefresh()
|
||||
{
|
||||
_autoRefresh = !_autoRefresh;
|
||||
if (_autoRefresh)
|
||||
{
|
||||
StartTimer();
|
||||
}
|
||||
else
|
||||
{
|
||||
_refreshTimer?.Dispose();
|
||||
_refreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsErrorExpanded(string deploymentId) => _expandedErrors.Contains(deploymentId);
|
||||
|
||||
private void ToggleErrorExpansion(string deploymentId)
|
||||
{
|
||||
if (!_expandedErrors.Remove(deploymentId))
|
||||
{
|
||||
_expandedErrors.Add(deploymentId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadDataAsync()
|
||||
|
||||
Reference in New Issue
Block a user