Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/DashboardDisplay.cs
T
Joseph Doherty 01033d7aaf
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 2m18s
ci / java (push) Successful in 2m24s
ci / portable (push) Successful in 8m51s
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.
2026-08-11 05:48:22 -04:00

89 lines
3.6 KiB
C#

namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Components;
public static class DashboardDisplay
{
/// <summary>
/// Formats a nullable date and time value for display.
/// </summary>
/// <param name="value">The date and time to format.</param>
/// <returns>Formatted date and time string or "-" if null.</returns>
public static string DateTime(DateTimeOffset? value)
{
return value.HasValue
? value.Value.UtcDateTime.ToString("yyyy-MM-dd HH:mm:ss 'UTC'", System.Globalization.CultureInfo.InvariantCulture)
: "-";
}
/// <summary>
/// Formats a time span duration for display.
/// </summary>
/// <param name="value">The duration to format.</param>
/// <returns>Formatted duration string.</returns>
public static string Duration(TimeSpan value)
{
return value.TotalDays >= 1
? value.ToString(@"d\.hh\:mm\:ss", System.Globalization.CultureInfo.InvariantCulture)
: value.ToString(@"hh\:mm\:ss", System.Globalization.CultureInfo.InvariantCulture);
}
/// <summary>
/// Formats a nullable text value for display.
/// </summary>
/// <param name="value">The text to format.</param>
/// <returns>Formatted text or "-" if null or empty.</returns>
public static string Text(string? value)
{
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>
/// <param name="value">The count to format.</param>
/// <returns>Formatted count string.</returns>
public static string Count(long value)
{
return value.ToString("N0", System.Globalization.CultureInfo.InvariantCulture);
}
/// <summary>
/// Retrieves a metric value from a snapshot by name and optional dimension.
/// </summary>
/// <param name="snapshot">Dashboard snapshot.</param>
/// <param name="name">Metric name.</param>
/// <param name="dimension">Optional metric dimension.</param>
/// <returns>Metric value or zero if not found.</returns>
public static long MetricValue(DashboardSnapshot snapshot, string name, string? dimension = null)
{
return snapshot.Metrics.FirstOrDefault(metric =>
string.Equals(metric.Name, name, StringComparison.Ordinal)
&& string.Equals(metric.Dimension, dimension, StringComparison.Ordinal))?.Value ?? 0;
}
}