Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/GalaxyPage.razor
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

193 lines
7.2 KiB
Plaintext

@page "/galaxy"
@inherits DashboardPageBase
@using ZB.MOM.WW.GalaxyRepository
<PageTitle>Dashboard Galaxy</PageTitle>
@if (Snapshot is null)
{
<div class="empty-state">Loading Galaxy summary.</div>
}
else
{
<div class="dashboard-page-header">
<div>
<h1>Galaxy Repository</h1>
<div class="text-secondary">@RefreshHeading()</div>
</div>
<StatusBadge Text="@Snapshot.Galaxy.Status.ToString()" />
</div>
<section class="metric-grid">
<MetricCard Label="Last Deploy" Value="@DashboardDisplay.DateTime(Snapshot.Galaxy.LastDeployTime)" Detail="@DeployAge()" Wide="true" />
<MetricCard Label="Last Refresh" Value="@DashboardDisplay.DateTime(Snapshot.Galaxy.LastSuccessAt)" Detail="@LastAttemptDetail()" Wide="true" />
<MetricCard Label="Objects" Value="@DashboardDisplay.Count(Snapshot.Galaxy.ObjectCount)" Detail="@($"{Snapshot.Galaxy.AreaCount:N0} areas")" />
<MetricCard Label="Attributes" Value="@DashboardDisplay.Count(Snapshot.Galaxy.AttributeCount)" Detail="dynamic, deployed" />
<MetricCard Label="Historized" Value="@DashboardDisplay.Count(Snapshot.Galaxy.HistorizedAttributeCount)" />
<MetricCard Label="Alarms" Value="@DashboardDisplay.Count(Snapshot.Galaxy.AlarmAttributeCount)" />
</section>
@if (Snapshot.Galaxy.Status == DashboardGalaxyStatus.Unknown)
{
<section class="dashboard-section">
<div class="empty-state">
Galaxy summary has not been collected yet. The dashboard refreshes the
summary every @RefreshIntervalSeconds() seconds via the
<code>GalaxyRepository</code> service.
</div>
</section>
}
@if (!string.IsNullOrWhiteSpace(Snapshot.Galaxy.LastError))
{
<section class="dashboard-section">
<div class="section-heading">
<h2>Last Error</h2>
</div>
<div class="empty-state">@Snapshot.Galaxy.LastError</div>
</section>
}
<section class="dashboard-section">
<div class="section-heading">
<h2>Object Categories</h2>
</div>
@if (Snapshot.Galaxy.ObjectCategories.Count == 0)
{
<div class="empty-state">No deployed objects observed.</div>
}
else
{
<div class="table-responsive">
<table class="table table-sm dashboard-table">
<thead>
<tr>
<th scope="col">Category</th>
<th scope="col">Category ID</th>
<th scope="col">Objects</th>
</tr>
</thead>
<tbody>
@foreach (DashboardGalaxyCategoryCount row in Snapshot.Galaxy.ObjectCategories)
{
<tr>
<td>@row.CategoryName</td>
<td><code>@row.CategoryId</code></td>
<td>@DashboardDisplay.Count(row.ObjectCount)</td>
</tr>
}
</tbody>
</table>
</div>
}
</section>
<section class="dashboard-section">
<div class="section-heading">
<h2>Top Templates</h2>
</div>
@if (Snapshot.Galaxy.TopTemplates.Count == 0)
{
<div class="empty-state">No template usage observed.</div>
}
else
{
<div class="table-responsive">
<table class="table table-sm dashboard-table">
<thead>
<tr>
<th scope="col">Template</th>
<th scope="col">Instances</th>
</tr>
</thead>
<tbody>
@foreach (DashboardGalaxyTemplateUsage row in Snapshot.Galaxy.TopTemplates)
{
<tr>
<td><code>@row.TemplateName</code></td>
<td>@DashboardDisplay.Count(row.InstanceCount)</td>
</tr>
}
</tbody>
</table>
</div>
}
</section>
<section class="dashboard-section">
<div class="section-heading">
<h2>Sync Info</h2>
</div>
<div class="table-responsive">
<table class="table table-sm dashboard-table details-table">
<tbody>
<tr><th scope="row">Status</th><td><StatusBadge Text="@Snapshot.Galaxy.Status.ToString()" /></td></tr>
<tr><th scope="row">Last successful refresh</th><td>@DashboardDisplay.DateTime(Snapshot.Galaxy.LastSuccessAt)</td></tr>
<tr><th scope="row">Last attempt</th><td>@DashboardDisplay.DateTime(Snapshot.Galaxy.LastQueriedAt)</td></tr>
<tr><th scope="row">Galaxy <code>time_of_last_deploy</code></th><td>@DashboardDisplay.DateTime(Snapshot.Galaxy.LastDeployTime)</td></tr>
<tr><th scope="row">Refresh interval</th><td>@RefreshIntervalSeconds() seconds</td></tr>
<tr><th scope="row">Connection string</th><td><code>@DashboardDisplay.Text(GalaxyConnectionStringDisplay())</code></td></tr>
<tr><th scope="row">Command timeout</th><td>@CommandTimeoutSeconds() seconds</td></tr>
</tbody>
</table>
</div>
</section>
}
@code {
[Inject]
private IOptions<GalaxyRepositoryOptions> GalaxyOptions { get; set; } = null!;
private string RefreshHeading()
{
DashboardGalaxySummary galaxy = Snapshot!.Galaxy;
return galaxy.LastSuccessAt is null
? "Awaiting first successful refresh"
: $"Refreshed {DashboardDisplay.DateTime(galaxy.LastSuccessAt)}";
}
private string? DeployAge()
{
DashboardGalaxySummary galaxy = Snapshot!.Galaxy;
if (galaxy.LastDeployTime is null || galaxy.LastSuccessAt is null)
{
return null;
}
TimeSpan age = galaxy.LastSuccessAt.Value - galaxy.LastDeployTime.Value;
if (age < TimeSpan.Zero)
{
return null;
}
return $"{DashboardDisplay.Duration(age)} ago";
}
private string? LastAttemptDetail()
{
DashboardGalaxySummary galaxy = Snapshot!.Galaxy;
if (galaxy.LastQueriedAt is null)
{
return "never queried";
}
if (galaxy.LastSuccessAt is null)
{
return "no successful refresh yet";
}
return galaxy.LastQueriedAt > galaxy.LastSuccessAt
? $"last attempt {DashboardDisplay.DateTime(galaxy.LastQueriedAt)}"
: null;
}
private int RefreshIntervalSeconds() => Math.Max(1, GalaxyOptions.Value.DashboardRefreshIntervalSeconds);
private int CommandTimeoutSeconds() => GalaxyOptions.Value.CommandTimeoutSeconds;
private string GalaxyConnectionStringDisplay()
{
return DashboardConnectionStringDisplay.GalaxyRepositoryConnectionString(GalaxyOptions.Value.ConnectionString);
}
}