fix(raw): tag edit modal feeds the picker the merged device config, not '{}' (#468 follow-up)
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
passed in and drives the typed TagConfig editor dispatch (TagConfigEditorMap; raw-JSON fallback for
|
||||
unmapped drivers, e.g. Galaxy). Edit-only: the host opens it for an existing tree node (TagId), it
|
||||
loads via LoadTagForEditAsync, and saves via UpdateTagAsync. Visibility is @bind-Visible; a successful
|
||||
save raises OnSaved so the coordinator can refresh the tree. *@
|
||||
save raises OnSaved so the coordinator can refresh the tree. The editor also receives the merged
|
||||
Driver+Device probe config, which is what its address picker dials when browsing the live device. *@
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||
@@ -140,11 +141,17 @@
|
||||
// reload + clobber in-progress edits. Null while closed.
|
||||
private string? _loadedKey;
|
||||
|
||||
// Merged Driver+Device config for the typed editor's address picker — the same blob Test-connect and
|
||||
// RawBrowseModal dial, resolved once per open. Null (→ "{}") when the resolve failed: the picker's
|
||||
// browse then degrades to its graceful error chip.
|
||||
private string? _mergedConfigJson;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (!Visible)
|
||||
{
|
||||
_loadedKey = null; // closed → next open reloads fresh
|
||||
_mergedConfigJson = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,14 +187,31 @@
|
||||
TagConfig = string.IsNullOrWhiteSpace(d.TagConfig) ? "{}" : d.TagConfig,
|
||||
};
|
||||
_rowVersion = d.RowVersion;
|
||||
|
||||
_mergedConfigJson = null;
|
||||
try
|
||||
{
|
||||
var merged = await Svc.LoadMergedProbeConfigAsync(d.DeviceId);
|
||||
if (merged is { } m)
|
||||
{
|
||||
_mergedConfigJson = m.MergedConfigJson;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fail-soft: the modal must open and save regardless of the probe-config resolve. Only the
|
||||
// browse affordance degrades, and it reports its own error when used.
|
||||
}
|
||||
}
|
||||
|
||||
private IDictionary<string, object> BuildEditorParameters() => new Dictionary<string, object>
|
||||
/// <summary>Parameters handed to the typed TagConfig editor. Internal so tests can assert them.</summary>
|
||||
internal IDictionary<string, object> BuildEditorParameters() => new Dictionary<string, object>
|
||||
{
|
||||
["ConfigJson"] = _form.TagConfig,
|
||||
["ConfigJsonChanged"] = EventCallback.Factory.Create<string>(this, v => _form.TagConfig = v),
|
||||
["DriverType"] = DriverType ?? "",
|
||||
["GetDriverConfigJson"] = (Func<string>)(() => "{}"),
|
||||
// Live accessor, not a snapshot: the picker reads it when the operator clicks Browse.
|
||||
["GetDriverConfigJson"] = (Func<string>)(() => _mergedConfigJson ?? "{}"),
|
||||
};
|
||||
|
||||
private async Task SaveAsync()
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
using System.Reflection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Raw;
|
||||
|
||||
/// <summary>
|
||||
/// Pins the #468 follow-up: the <c>/raw</c> tag edit modal must hand its typed TagConfig editor the
|
||||
/// merged Driver+Device config, not the historical <c>"{}"</c> stub.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The AbCip / TwinCAT / FOCAS tag editors embed an address picker whose "Browse controller"
|
||||
/// affordance dials the <c>GetDriverConfigJson</c> accessor as its connect config. While that
|
||||
/// accessor returned a literal <c>"{}"</c> the browse captured against an empty config and could
|
||||
/// never succeed — a defect no offline test could see, because the accessor was well-formed JSON.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// AdminUI has no bUnit, so the component is driven directly as a class: the injected
|
||||
/// <c>Svc</c> property and the protected <c>OnParametersSetAsync</c> lifecycle hook are reached by
|
||||
/// reflection, and the editor parameters are read back through the internal
|
||||
/// <c>BuildEditorParameters</c>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
// BL0005 forbids setting a [Parameter] from outside the component, which is right for production code
|
||||
// and impossible to honour here: with no bUnit there is no renderer to supply parameters, so driving the
|
||||
// component as a class means assigning them directly.
|
||||
#pragma warning disable BL0005
|
||||
public sealed class RawTagModalMergedConfigTests
|
||||
{
|
||||
private const string MergedConfig = """{"endpoint":"10.0.0.1:44818","slot":0}""";
|
||||
|
||||
[Fact]
|
||||
public async Task The_editor_receives_the_merged_device_config_not_a_stub()
|
||||
{
|
||||
var svc = new FakeRawTreeService
|
||||
{
|
||||
OnLoadTagForEdit = tagId => TagOn(tagId, "dev-1"),
|
||||
OnLoadMergedProbeConfig = deviceId =>
|
||||
deviceId == "dev-1" ? ("AbCip", MergedConfig) : null,
|
||||
};
|
||||
|
||||
var modal = NewModal(svc, "tag-1");
|
||||
await InvokeParametersSetAsync(modal);
|
||||
|
||||
DriverConfigJson(modal).ShouldBe(MergedConfig);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_failed_resolve_degrades_to_the_empty_stub_and_does_not_throw()
|
||||
{
|
||||
var svc = new FakeRawTreeService
|
||||
{
|
||||
OnLoadTagForEdit = tagId => TagOn(tagId, "dev-1"),
|
||||
OnLoadMergedProbeConfig = _ => throw new InvalidOperationException("device is gone"),
|
||||
};
|
||||
|
||||
var modal = NewModal(svc, "tag-1");
|
||||
|
||||
// The modal must still open and remain saveable — only the browse affordance degrades, and it
|
||||
// reports its own error when used.
|
||||
await Should.NotThrowAsync(() => InvokeParametersSetAsync(modal));
|
||||
|
||||
DriverConfigJson(modal).ShouldBe("{}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_accessor_is_live_not_a_captured_snapshot()
|
||||
{
|
||||
var svc = new FakeRawTreeService
|
||||
{
|
||||
OnLoadTagForEdit = tagId => TagOn(tagId, tagId == "tag-1" ? "dev-1" : "dev-2"),
|
||||
OnLoadMergedProbeConfig = _ => null,
|
||||
};
|
||||
|
||||
var modal = NewModal(svc, "tag-1");
|
||||
await InvokeParametersSetAsync(modal);
|
||||
|
||||
var firstAccessor = (Func<string>)modal.BuildEditorParameters()["GetDriverConfigJson"];
|
||||
firstAccessor().ShouldBe("{}");
|
||||
|
||||
// Close, then reopen on a different tag whose device now resolves. The per-open cache must have
|
||||
// been reset and re-resolved, and the accessor must read the field at call time rather than
|
||||
// having closed over the value it saw when it was built.
|
||||
modal.Visible = false;
|
||||
await InvokeParametersSetAsync(modal);
|
||||
|
||||
svc.OnLoadMergedProbeConfig = deviceId =>
|
||||
deviceId == "dev-2" ? ("AbCip", MergedConfig) : null;
|
||||
modal.Visible = true;
|
||||
modal.TagId = "tag-2";
|
||||
await InvokeParametersSetAsync(modal);
|
||||
|
||||
DriverConfigJson(modal).ShouldBe(MergedConfig);
|
||||
firstAccessor().ShouldBe(MergedConfig);
|
||||
}
|
||||
|
||||
private static RawTagEditDto TagOn(string tagId, string deviceId) => new(
|
||||
tagId,
|
||||
deviceId,
|
||||
TagGroupId: null,
|
||||
Name: "Speed",
|
||||
DataType: "Float",
|
||||
AccessLevel: TagAccessLevel.Read,
|
||||
WriteIdempotent: false,
|
||||
PollGroupId: null,
|
||||
TagConfig: "{}",
|
||||
RowVersion: []);
|
||||
|
||||
private static RawTagModal NewModal(IRawTreeService svc, string tagId)
|
||||
{
|
||||
var modal = new RawTagModal { Visible = true, TagId = tagId, DriverType = "AbCip" };
|
||||
|
||||
// @inject generates a private property, so the fake goes in by reflection.
|
||||
var injected = typeof(RawTagModal).GetProperty(
|
||||
"Svc",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
injected.ShouldNotBeNull("RawTagModal no longer injects IRawTreeService as 'Svc'.");
|
||||
injected!.SetValue(modal, svc);
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
private static Task InvokeParametersSetAsync(RawTagModal modal)
|
||||
{
|
||||
var hook = typeof(RawTagModal).GetMethod(
|
||||
"OnParametersSetAsync",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
hook.ShouldNotBeNull("RawTagModal no longer overrides OnParametersSetAsync.");
|
||||
|
||||
return (Task)hook!.Invoke(modal, null)!;
|
||||
}
|
||||
|
||||
private static string DriverConfigJson(RawTagModal modal)
|
||||
=> ((Func<string>)modal.BuildEditorParameters()["GetDriverConfigJson"])();
|
||||
}
|
||||
#pragma warning restore BL0005
|
||||
Reference in New Issue
Block a user