feat(adminui): refresh the browse tree after a rebirth request (G2-F3)
RequestRebirthAsync now returns only once the requested births have arrived and gone quiet, but the modal still told the operator to re-expand the tree in a few seconds. ConfirmRebirthAsync re-keys the tree against the same session (as the Refresh button does) when the session and modal are still current, drops the now-stale armed scope, and keeps the outcome line. Refs #745 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -683,13 +683,29 @@
|
||||
_rebirthOutcome = null;
|
||||
_rebirthError = null;
|
||||
StateHasChanged();
|
||||
var token = _token;
|
||||
var target = _rebirthTarget;
|
||||
try
|
||||
{
|
||||
var published = await BrowserService.RequestRebirthAsync(_token, _rebirthTarget.Scope, default);
|
||||
var published = await BrowserService.RequestRebirthAsync(token, target.Scope, default);
|
||||
// The session's RequestRebirthAsync returns only once the births it asked for have arrived and
|
||||
// gone quiet (bounded by the window), so the tree can be re-read now instead of telling the
|
||||
// operator to re-expand it later (G2-F3). Skipped if the session or modal went away meanwhile.
|
||||
var refreshed = token == _token && Visible && !_disposed;
|
||||
if (refreshed)
|
||||
{
|
||||
// Re-key the tree against the SAME session (as the Refresh button does). The armed scope is
|
||||
// dropped — its node may not be in the rebuilt tree — but the outcome line is kept.
|
||||
_treeGeneration++;
|
||||
_rebirthTarget = null;
|
||||
}
|
||||
|
||||
_rebirthOutcome =
|
||||
$"Rebirth requested for {_rebirthTarget.Kind} — {published} command"
|
||||
$"Rebirth requested for {target.Kind} — {published} command"
|
||||
+ (published == 1 ? "" : "s")
|
||||
+ " published. Re-expand the tree in a few seconds to see the republished metrics.";
|
||||
+ (refreshed
|
||||
? " published. The tree was refreshed with the births that arrived; press Refresh if more appear later."
|
||||
: " published.");
|
||||
_rebirthConfirming = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Browsing;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Raw;
|
||||
|
||||
// BL0006: the test renderer needs RenderTree types (same suppression as RawBrowseModalStaleOpenTests).
|
||||
#pragma warning disable BL0006
|
||||
|
||||
/// <summary>
|
||||
/// G2-F3: after a rebirth request the browse modal re-reads the tree itself (the session's
|
||||
/// <c>RequestRebirthAsync</c> now waits for the births), instead of telling the operator to re-expand it.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class RawBrowseRebirthRefreshTests : IDisposable
|
||||
{
|
||||
private const BindingFlags Private = BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
private readonly List<Renderer> _renderers = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var r in _renderers) r.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_completed_rebirth_rekeys_the_tree_and_keeps_the_outcome()
|
||||
{
|
||||
var svc = new RebirthBrowserService { Result = Task.FromResult(2) };
|
||||
var modal = NewOpenModal(svc);
|
||||
var before = Get<int>(modal, "_treeGeneration");
|
||||
|
||||
await ConfirmAsync(modal);
|
||||
|
||||
Get<int>(modal, "_treeGeneration").ShouldBe(before + 1);
|
||||
Get<string?>(modal, "_rebirthOutcome")!.ShouldContain("The tree was refreshed");
|
||||
Get<object?>(modal, "_rebirthTarget").ShouldBeNull();
|
||||
svc.Scopes.ShouldBe(["Grp/EdgeA"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_modal_closed_during_the_wait_is_not_refreshed()
|
||||
{
|
||||
var gate = new TaskCompletionSource<int>();
|
||||
var svc = new RebirthBrowserService { Result = gate.Task };
|
||||
var modal = NewOpenModal(svc);
|
||||
var before = Get<int>(modal, "_treeGeneration");
|
||||
|
||||
var confirm = ConfirmAsync(modal);
|
||||
typeof(RawBrowseModal).GetProperty("Visible")!.SetValue(modal, false);
|
||||
gate.SetResult(1);
|
||||
await confirm;
|
||||
|
||||
Get<int>(modal, "_treeGeneration").ShouldBe(before);
|
||||
Get<string?>(modal, "_rebirthOutcome")!.ShouldNotContain("refreshed");
|
||||
// Positive control: the publish itself was still made and reported.
|
||||
svc.Scopes.ShouldBe(["Grp/EdgeA"]);
|
||||
}
|
||||
|
||||
private RawBrowseModal NewOpenModal(IBrowserSessionService svc)
|
||||
{
|
||||
var modal = new RawBrowseModal();
|
||||
typeof(RawBrowseModal).GetProperty("BrowserService", Private)!.SetValue(modal, svc);
|
||||
typeof(RawBrowseModal).GetProperty("Visible")!.SetValue(modal, true);
|
||||
Set(modal, "_token", Guid.NewGuid());
|
||||
Set(modal, "_canRebirth", true);
|
||||
|
||||
var targetType = typeof(RawBrowseModal).GetNestedType("RebirthTarget", BindingFlags.NonPublic)!;
|
||||
Set(modal, "_rebirthTarget", Activator.CreateInstance(targetType, "Grp/EdgeA", "edge node EdgeA", "detail", false));
|
||||
Set(modal, "_rebirthConfirming", true);
|
||||
|
||||
var renderer = new InertRenderer();
|
||||
_renderers.Add(renderer);
|
||||
renderer.Attach(modal);
|
||||
return modal;
|
||||
}
|
||||
|
||||
private static Task ConfirmAsync(RawBrowseModal modal) =>
|
||||
(Task)typeof(RawBrowseModal).GetMethod("ConfirmRebirthAsync", Private)!.Invoke(modal, null)!;
|
||||
|
||||
private static void Set(RawBrowseModal modal, string field, object? value)
|
||||
{
|
||||
var f = typeof(RawBrowseModal).GetField(field, Private);
|
||||
f.ShouldNotBeNull($"{field} was renamed");
|
||||
f!.SetValue(modal, value);
|
||||
}
|
||||
|
||||
private static T Get<T>(RawBrowseModal modal, string field) =>
|
||||
(T)typeof(RawBrowseModal).GetField(field, Private)!.GetValue(modal)!;
|
||||
|
||||
private sealed class RebirthBrowserService : IBrowserSessionService
|
||||
{
|
||||
public required Task<int> Result { get; init; }
|
||||
public List<string> Scopes { get; } = new();
|
||||
|
||||
public bool CanBrowse(string driverType, string configJson) => true;
|
||||
public Task<BrowseOpenResult> OpenAsync(string driverType, string configJson, CancellationToken ct) => throw new NotImplementedException();
|
||||
public Task<IReadOnlyList<BrowseNode>> RootAsync(Guid token, CancellationToken ct) => throw new NotImplementedException();
|
||||
public Task<IReadOnlyList<BrowseNode>> ExpandAsync(Guid token, string nodeId, CancellationToken ct) => throw new NotImplementedException();
|
||||
public Task<IReadOnlyList<AttributeInfo>> AttributesAsync(Guid token, string nodeId, CancellationToken ct) => throw new NotImplementedException();
|
||||
public bool CanRequestRebirth(Guid token) => true;
|
||||
public Task CloseAsync(Guid token) => Task.CompletedTask;
|
||||
|
||||
public Task<int> RequestRebirthAsync(Guid token, string scope, CancellationToken ct)
|
||||
{
|
||||
Scopes.Add(scope);
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InertRenderer()
|
||||
: Renderer(new ServiceCollection().BuildServiceProvider(), NullLoggerFactory.Instance)
|
||||
{
|
||||
public override Dispatcher Dispatcher { get; } = new InlineDispatcher();
|
||||
public void Attach(IComponent component) => AssignRootComponentId(component);
|
||||
protected override void HandleException(Exception exception) => ExceptionDispatchInfo.Capture(exception).Throw();
|
||||
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class InlineDispatcher : Dispatcher
|
||||
{
|
||||
public override bool CheckAccess() => true;
|
||||
public override Task InvokeAsync(Action workItem) { workItem(); return Task.CompletedTask; }
|
||||
public override Task InvokeAsync(Func<Task> workItem) => workItem();
|
||||
public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem) => Task.FromResult(workItem());
|
||||
public override Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem) => workItem();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user