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.
168 lines
6.1 KiB
Plaintext
168 lines
6.1 KiB
Plaintext
@using ZB.MOM.WW.GalaxyRepository.Grpc
|
|
|
|
@*
|
|
Recursive Browse hierarchy node. Renders one Galaxy object, its child
|
|
objects (recursively, lazy-loaded on first expand), and its attributes as
|
|
right-clickable tag rows. Expansion state is local; children render only
|
|
while expanded.
|
|
|
|
The expand triangle is shown whenever the server's child_has_children
|
|
projector hint is set (HasChildrenHint), even before children have been
|
|
loaded — clicking it triggers OnLoadChildren so the parent page can fill
|
|
in Node.Children, then the view re-renders.
|
|
*@
|
|
|
|
<div class="tree-node">
|
|
<div class="tree-row @(Node.IsArea ? "tree-row-area" : "tree-row-object")">
|
|
@if (ShowToggle())
|
|
{
|
|
<button type="button" class="tree-toggle" @onclick="ToggleAsync" aria-label="Toggle">
|
|
@(_expanded ? "▾" : "▸")
|
|
</button>
|
|
}
|
|
else
|
|
{
|
|
<span class="tree-toggle tree-toggle-empty"></span>
|
|
}
|
|
<span class="tree-label" @onclick="ToggleAsync">
|
|
<span class="tree-icon">@(Node.IsArea ? "▣" : "◇")</span>
|
|
<span class="tree-name">@Node.DisplayName</span>
|
|
@if (!string.IsNullOrWhiteSpace(Node.Object.TagName)
|
|
&& !string.Equals(Node.Object.TagName, Node.DisplayName, StringComparison.Ordinal))
|
|
{
|
|
<code class="tree-tag">@Node.Object.TagName</code>
|
|
}
|
|
</span>
|
|
</div>
|
|
@if (_expanded)
|
|
{
|
|
<div class="tree-children">
|
|
@if (Node.LoadState == BrowseLoadState.Loading)
|
|
{
|
|
<div class="tree-load-status text-secondary">
|
|
<span class="tree-toggle tree-toggle-empty"></span>
|
|
<span>⌛ Loading…</span>
|
|
</div>
|
|
}
|
|
else if (Node.LoadState == BrowseLoadState.Error)
|
|
{
|
|
@* 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: @DashboardDisplay.Abbreviate(Node.LoadError, 60)</span>
|
|
</div>
|
|
}
|
|
|
|
@foreach (DashboardBrowseNode child in Node.Children)
|
|
{
|
|
<BrowseTreeNodeView Node="child"
|
|
OnAddTag="OnAddTag"
|
|
OnTagContextMenu="OnTagContextMenu"
|
|
OnLoadChildren="OnLoadChildren" />
|
|
}
|
|
@foreach (GalaxyAttribute attr in Node.Attributes)
|
|
{
|
|
GalaxyAttribute row = attr;
|
|
<div class="tree-attr"
|
|
title="@row.FullTagReference"
|
|
@oncontextmenu:preventDefault="true"
|
|
@oncontextmenu="@(args => OnTagContextMenu.InvokeAsync((args, row)))"
|
|
@ondblclick="@(() => OnAddTag.InvokeAsync(row.FullTagReference))">
|
|
<span class="tree-toggle tree-toggle-empty"></span>
|
|
<span class="attr-icon">·</span>
|
|
<span class="attr-name">@row.AttributeName</span>
|
|
<span class="attr-type">@DisplayType(row)</span>
|
|
@if (row.IsAlarm)
|
|
{
|
|
<span class="attr-flag attr-flag-alarm">alarm</span>
|
|
}
|
|
@if (row.IsHistorized)
|
|
{
|
|
<span class="attr-flag attr-flag-hist">hist</span>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@code {
|
|
/// <summary>The hierarchy node this view renders.</summary>
|
|
[Parameter]
|
|
[EditorRequired]
|
|
public DashboardBrowseNode Node { get; set; } = null!;
|
|
|
|
/// <summary>Raised with a tag's full reference when the operator double-clicks it.</summary>
|
|
[Parameter]
|
|
public EventCallback<string> OnAddTag { get; set; }
|
|
|
|
/// <summary>Raised when an attribute row is right-clicked, for the context menu.</summary>
|
|
[Parameter]
|
|
public EventCallback<(MouseEventArgs Event, GalaxyAttribute Attribute)> OnTagContextMenu { get; set; }
|
|
|
|
/// <summary>
|
|
/// Invoked on first expand when the projector hint says this node has children
|
|
/// but they have not been fetched yet. The callback is expected to populate
|
|
/// <see cref="DashboardBrowseNode.Children"/> on the node it receives and then
|
|
/// trigger a re-render.
|
|
/// </summary>
|
|
[Parameter]
|
|
public Func<DashboardBrowseNode, Task>? OnLoadChildren { get; set; }
|
|
|
|
private bool _expanded;
|
|
|
|
// The triangle is shown whenever the projector says children exist (even
|
|
// pre-load), or attributes are already present, or already-loaded children
|
|
// are sitting on the node.
|
|
private bool ShowToggle()
|
|
{
|
|
return Node.HasChildrenHint
|
|
|| Node.Attributes.Count > 0
|
|
|| Node.Children.Count > 0;
|
|
}
|
|
|
|
private async Task ToggleAsync()
|
|
{
|
|
if (!ShowToggle())
|
|
{
|
|
return;
|
|
}
|
|
|
|
_expanded = !_expanded;
|
|
|
|
if (_expanded
|
|
&& Node.HasChildrenHint
|
|
&& Node.LoadState == BrowseLoadState.NotLoaded
|
|
&& OnLoadChildren is not null)
|
|
{
|
|
Node.LoadState = BrowseLoadState.Loading;
|
|
try
|
|
{
|
|
await OnLoadChildren(Node);
|
|
Node.LoadState = BrowseLoadState.Loaded;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Node.LoadState = BrowseLoadState.Error;
|
|
Node.LoadError = ex.Message;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string DisplayType(GalaxyAttribute attribute)
|
|
{
|
|
string baseType = string.IsNullOrWhiteSpace(attribute.DataTypeName)
|
|
? "type?"
|
|
: attribute.DataTypeName;
|
|
if (attribute.IsArray)
|
|
{
|
|
return attribute.ArrayDimensionPresent
|
|
? $"{baseType}[{attribute.ArrayDimension}]"
|
|
: $"{baseType}[]";
|
|
}
|
|
|
|
return baseType;
|
|
}
|
|
}
|