01033d7aaf
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.
193 lines
7.2 KiB
Plaintext
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);
|
|
}
|
|
}
|