dashboard: lazy-load BrowsePage via DashboardBrowseService
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
@implements IAsyncDisposable
|
||||
@inject IGalaxyHierarchyCache GalaxyCache
|
||||
@inject IDashboardLiveDataService LiveData
|
||||
@inject IDashboardBrowseService BrowseService
|
||||
@inject IGalaxyDeployNotifier DeployNotifier
|
||||
@using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy
|
||||
@using ZB.MOM.WW.MxGateway.Server.Galaxy
|
||||
|
||||
@@ -71,12 +73,18 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (!string.IsNullOrEmpty(_staleBanner))
|
||||
{
|
||||
<div class="alert alert-info browse-stale-banner" role="status"
|
||||
@onclick="ClearStaleBanner">@_staleBanner</div>
|
||||
}
|
||||
<div class="browse-tree">
|
||||
@foreach (DashboardBrowseNode root in _roots)
|
||||
{
|
||||
<BrowseTreeNodeView Node="root"
|
||||
OnAddTag="AddTagAsync"
|
||||
OnTagContextMenu="OnTagContextMenu" />
|
||||
OnTagContextMenu="OnTagContextMenu"
|
||||
OnLoadChildren="LoadChildrenAsync" />
|
||||
}
|
||||
</div>
|
||||
<div class="browse-search-note">Double-click a tag, or right-click for the menu.</div>
|
||||
@@ -186,7 +194,11 @@
|
||||
@code {
|
||||
private const int SearchResultLimit = 300;
|
||||
|
||||
private IReadOnlyList<DashboardBrowseNode> _roots = [];
|
||||
private List<DashboardBrowseNode> _roots = [];
|
||||
private ulong _cacheSequence;
|
||||
private string? _staleBanner;
|
||||
private CancellationTokenSource _deployCts = new();
|
||||
private Task? _deployTask;
|
||||
private string _search = string.Empty;
|
||||
private IReadOnlyList<GalaxyAttribute> _searchMatches = [];
|
||||
private readonly List<string> _subscribed = [];
|
||||
@@ -210,8 +222,58 @@
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_roots = DashboardBrowseTreeBuilder.Build(GalaxyCache.Current.Objects);
|
||||
BrowseLevelResult roots = BrowseService.GetRoots(new BrowseFilterArgs());
|
||||
_roots = [.. roots.Nodes];
|
||||
_cacheSequence = roots.CacheSequence;
|
||||
_pollTask = PollLoopAsync();
|
||||
_deployTask = SubscribeToDeployEventsAsync();
|
||||
}
|
||||
|
||||
private async Task LoadChildrenAsync(DashboardBrowseNode node)
|
||||
{
|
||||
BrowseLevelResult result = BrowseService.GetChildren(node.Object.GobjectId, new BrowseFilterArgs());
|
||||
node.Children.Clear();
|
||||
foreach (DashboardBrowseNode child in result.Nodes)
|
||||
{
|
||||
node.Children.Add(child);
|
||||
}
|
||||
|
||||
// First expand interaction also dismisses the stale banner — the user
|
||||
// is clearly engaging with the tree, no need to keep nagging.
|
||||
_staleBanner = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task SubscribeToDeployEventsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (GalaxyDeployEventInfo info in DeployNotifier
|
||||
.SubscribeAsync(_deployCts.Token)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
// First Latest replay echoes the sequence we already projected
|
||||
// from — skip those to avoid a spurious "redeployed" banner.
|
||||
if (info.Sequence == 0 || (ulong)info.Sequence == _cacheSequence)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
BrowseLevelResult roots = BrowseService.GetRoots(new BrowseFilterArgs());
|
||||
_roots = [.. roots.Nodes];
|
||||
_cacheSequence = roots.CacheSequence;
|
||||
_staleBanner = "Galaxy redeployed — tree refreshed.";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearStaleBanner()
|
||||
{
|
||||
_staleBanner = null;
|
||||
}
|
||||
|
||||
private string HeaderLine()
|
||||
@@ -405,6 +467,7 @@
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
await _deployCts.CancelAsync();
|
||||
if (_pollTask is not null)
|
||||
{
|
||||
try
|
||||
@@ -415,8 +478,19 @@
|
||||
{
|
||||
}
|
||||
}
|
||||
if (_deployTask is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _deployTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
_deployCts.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
+72
-9
@@ -2,15 +2,21 @@
|
||||
|
||||
@*
|
||||
Recursive Browse hierarchy node. Renders one Galaxy object, its child
|
||||
objects (recursively), and its attributes as right-clickable tag rows.
|
||||
Expansion state is local; children render only while expanded.
|
||||
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 (Node.HasChildren)
|
||||
@if (ShowToggle())
|
||||
{
|
||||
<button type="button" class="tree-toggle" @onclick="Toggle" aria-label="Toggle">
|
||||
<button type="button" class="tree-toggle" @onclick="ToggleAsync" aria-label="Toggle">
|
||||
@(_expanded ? "▾" : "▸")
|
||||
</button>
|
||||
}
|
||||
@@ -18,7 +24,7 @@
|
||||
{
|
||||
<span class="tree-toggle tree-toggle-empty"></span>
|
||||
}
|
||||
<span class="tree-label" @onclick="Toggle">
|
||||
<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)
|
||||
@@ -31,9 +37,27 @@
|
||||
@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)
|
||||
{
|
||||
<div class="tree-load-status text-danger">
|
||||
<span class="tree-toggle tree-toggle-empty"></span>
|
||||
<span>Failed to load: @Node.LoadError</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@foreach (DashboardBrowseNode child in Node.Children)
|
||||
{
|
||||
<BrowseTreeNodeView Node="child" OnAddTag="OnAddTag" OnTagContextMenu="OnTagContextMenu" />
|
||||
<BrowseTreeNodeView Node="child"
|
||||
OnAddTag="OnAddTag"
|
||||
OnTagContextMenu="OnTagContextMenu"
|
||||
OnLoadChildren="OnLoadChildren" />
|
||||
}
|
||||
@foreach (GalaxyAttribute attr in Node.Attributes)
|
||||
{
|
||||
@@ -75,13 +99,52 @@
|
||||
[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;
|
||||
|
||||
private void Toggle()
|
||||
// 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()
|
||||
{
|
||||
if (Node.HasChildren)
|
||||
return Node.HasChildrenHint
|
||||
|| Node.Attributes.Count > 0
|
||||
|| Node.Children.Count > 0;
|
||||
}
|
||||
|
||||
private async Task ToggleAsync()
|
||||
{
|
||||
if (!ShowToggle())
|
||||
{
|
||||
_expanded = !_expanded;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user