fix(dashboard): admin-UI cleanup sweep
Family-wide admin-UI cleanup pass (scadaproj admin_ui_cleanup.md) applied to the Blazor dashboard. Behaviour is unchanged throughout — no @onclick, disabled, binding, auth gate, or arm->confirm flow was touched. Uncontrolled error text is now truncated at the render site. Fault messages, Galaxy load errors, and browse-tree load failures were rendered in full into fixed-width table cells, where a long exception string blows out the column. Each site gets DashboardDisplay.Abbreviate plus a title attribute carrying the untruncated text, so nothing becomes unreachable. Abbreviate is length-checked rather than a bare range slice: `value[..n]` on a shorter string throws and takes the whole page render down with it. The two detail views whose entire purpose is to show one fault in full — SessionDetailsPage and GalaxyPage's Last Error — are deliberately left untruncated. Two classes referenced from markup had no definition anywhere in the sheet. .browse-stale-banner was inert; .tree-load-status was a real visual defect — loading and failed-to-load rows sit among .tree-row siblings and carry the same leading .tree-toggle-empty spacer, but that spacer only takes its width as a flex item, so without a flex container those rows lost their indent. Confirm/cancel pairs in ConfirmDialog and the API-key create form are now btn-groups with role="group" and an aria-label, replacing margin-spaced loose buttons. Removes a paragraph on GalaxyPage naming internal RPCs (DiscoverHierarchy, GetLastDeployTime) — implementation detail with no meaning to a dashboard operator. Verified in a real browser, not bUnit: full build clean, 879/879 tests, and a live gate against a running dashboard with a genuine ~250-char SqlClient exception as the erroring row. Results per check, including the checks that could NOT be exercised without an x86 worker, are recorded in docs/plans/2026-08-11-dashboard-ui-sweeps.md. That plan doc also records a correction: this app is NOT Bootstrap-free. The sweep brief said it was, citing the scadaproj index; libman.json pins bootstrap 5.3.3 and App.razor:7 links it ahead of the theme. The stale claim had already cost this app one skipped family sweep (scadaproj#2, the /admin/secrets modal), so that modal was live-gated here too and passes.
This commit is contained in:
@@ -36,6 +36,32 @@ public static class DashboardDisplay
|
||||
return string.IsNullOrWhiteSpace(value) ? "-" : value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a nullable text value for display, shortened to a maximum length.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For table cells bound to text the gateway does not control — fault messages, COM
|
||||
/// exception text, SQL errors. One multi-line exception otherwise makes a single row
|
||||
/// several times taller than its neighbours. Call sites keep the full text reachable
|
||||
/// on the element's <c>title</c> and on the row's detail page.
|
||||
/// </remarks>
|
||||
/// <param name="value">The text to format.</param>
|
||||
/// <param name="maxLength">Maximum characters to render before the ellipsis.</param>
|
||||
/// <returns>Formatted text, ellipsized when longer than <paramref name="maxLength"/>, or "-" if null or empty.</returns>
|
||||
public static string Abbreviate(string? value, int maxLength = 80)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
|
||||
// Length-checked, never a bare range slice: a value shorter than maxLength
|
||||
// would throw and take the whole page render down with it.
|
||||
return value.Length <= maxLength
|
||||
? value
|
||||
: string.Concat(value.AsSpan(0, maxLength).TrimEnd(), "…");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a long count value for display with thousands separator.
|
||||
/// </summary>
|
||||
|
||||
@@ -133,8 +133,10 @@ else
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-success btn-sm me-1" disabled="@IsBusy">Save</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" disabled="@IsBusy" @onclick="CloseCreateDialog">Cancel</button>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Create API key actions">
|
||||
<button type="submit" class="btn btn-success" disabled="@IsBusy">Save</button>
|
||||
<button type="button" class="btn btn-outline-secondary" disabled="@IsBusy" @onclick="CloseCreateDialog">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,8 @@ else
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(Snapshot.Galaxy.LastError))
|
||||
{
|
||||
<div class="empty-state mt-2">@Snapshot.Galaxy.LastError</div>
|
||||
@* Overview stays compact; the Galaxy page renders the error in full. *@
|
||||
<div class="empty-state mt-2" title="@Snapshot.Galaxy.LastError">@DashboardDisplay.Abbreviate(Snapshot.Galaxy.LastError, 160)</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
|
||||
@@ -131,11 +131,6 @@ else
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="text-secondary small mt-2">
|
||||
Browse data is served by the <code>galaxy_repository.v1.GalaxyRepository</code> gRPC
|
||||
service. Clients call <code>DiscoverHierarchy</code> for the full tree and
|
||||
<code>GetLastDeployTime</code> to detect redeployments.
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,8 @@ else
|
||||
<td>@DashboardDisplay.DateTime(session.OpenedAt)</td>
|
||||
<td>@DashboardDisplay.DateTime(session.LastClientActivityAt)</td>
|
||||
<td>@DashboardDisplay.DateTime(session.LastWorkerHeartbeatAt)</td>
|
||||
<td>@DashboardDisplay.Text(session.LastFault)</td>
|
||||
@* Full text stays reachable on the tooltip and on the session detail page. *@
|
||||
<td title="@session.LastFault">@DashboardDisplay.Abbreviate(session.LastFault)</td>
|
||||
@if (CanManage)
|
||||
{
|
||||
<td>
|
||||
|
||||
@@ -67,7 +67,8 @@ else
|
||||
<td><StatusBadge Text="@worker.State.ToString()" /></td>
|
||||
<td><NavLink href="@($"sessions/{Uri.EscapeDataString(worker.SessionId)}")"><code>@worker.SessionId</code></NavLink></td>
|
||||
<td>@DashboardDisplay.DateTime(worker.LastHeartbeatAt)</td>
|
||||
<td>@DashboardDisplay.Text(worker.LastFault)</td>
|
||||
@* Full text stays reachable on the tooltip and on the session detail page. *@
|
||||
<td title="@worker.LastFault">@DashboardDisplay.Abbreviate(worker.LastFault)</td>
|
||||
@if (CanManage)
|
||||
{
|
||||
<td>
|
||||
|
||||
@@ -46,9 +46,11 @@
|
||||
}
|
||||
else if (Node.LoadState == BrowseLoadState.Error)
|
||||
{
|
||||
<div class="tree-load-status text-danger">
|
||||
@* Abbreviated: sibling tree rows are nowrap inside a fixed-height
|
||||
scroller, so a full COM/SQL error would stretch the whole pane. *@
|
||||
<div class="tree-load-status text-danger" title="@Node.LoadError">
|
||||
<span class="tree-toggle tree-toggle-empty"></span>
|
||||
<span>Failed to load: @Node.LoadError</span>
|
||||
<span>Failed to load: @DashboardDisplay.Abbreviate(Node.LoadError, 60)</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -14,16 +14,18 @@
|
||||
<p class="mb-0">@Message</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
disabled="@IsBusy"
|
||||
@onclick="OnCancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn @ConfirmButtonClass"
|
||||
disabled="@IsBusy"
|
||||
@onclick="OnConfirm">
|
||||
@ConfirmLabel
|
||||
</button>
|
||||
<div class="btn-group" role="group" aria-label="Confirm or cancel">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
disabled="@IsBusy"
|
||||
@onclick="OnCancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn @ConfirmButtonClass"
|
||||
disabled="@IsBusy"
|
||||
@onclick="OnConfirm">
|
||||
@ConfirmLabel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ else
|
||||
<td><code>@DashboardDisplay.Text(fault.SessionId)</code></td>
|
||||
<td>@(fault.WorkerProcessId?.ToString(System.Globalization.CultureInfo.InvariantCulture) ?? "-")</td>
|
||||
<td><StatusBadge Text="@fault.State" /></td>
|
||||
<td>@fault.Message</td>
|
||||
<td title="@fault.Message">@DashboardDisplay.Abbreviate(fault.Message)</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
||||
@@ -301,6 +301,14 @@ code {
|
||||
.browse-panel { margin-top: 0; }
|
||||
|
||||
.browse-search { margin-bottom: 0.6rem; }
|
||||
|
||||
/* Click-to-dismiss "Galaxy redeployed" notice above the tree. The whole banner
|
||||
is the dismiss target, so it has to read as one. */
|
||||
.browse-stale-banner {
|
||||
cursor: pointer;
|
||||
padding: 0.4rem 0.7rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.browse-search-note {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.74rem;
|
||||
@@ -345,6 +353,22 @@ code {
|
||||
}
|
||||
.tree-toggle-empty { cursor: default; }
|
||||
|
||||
/* Loading / failed-to-load rows sit among .tree-row siblings and carry the same
|
||||
leading .tree-toggle-empty spacer. The spacer only takes its width as a flex
|
||||
item, so this container has to be flex or the row loses its indent. */
|
||||
.tree-load-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.12rem 0.5rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.tree-load-status > span:not(.tree-toggle) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tree-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user