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
|
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
|
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
|
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 System.ComponentModel.DataAnnotations
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||||
@@ -140,11 +141,17 @@
|
|||||||
// reload + clobber in-progress edits. Null while closed.
|
// reload + clobber in-progress edits. Null while closed.
|
||||||
private string? _loadedKey;
|
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()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
if (!Visible)
|
if (!Visible)
|
||||||
{
|
{
|
||||||
_loadedKey = null; // closed → next open reloads fresh
|
_loadedKey = null; // closed → next open reloads fresh
|
||||||
|
_mergedConfigJson = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,14 +187,31 @@
|
|||||||
TagConfig = string.IsNullOrWhiteSpace(d.TagConfig) ? "{}" : d.TagConfig,
|
TagConfig = string.IsNullOrWhiteSpace(d.TagConfig) ? "{}" : d.TagConfig,
|
||||||
};
|
};
|
||||||
_rowVersion = d.RowVersion;
|
_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,
|
["ConfigJson"] = _form.TagConfig,
|
||||||
["ConfigJsonChanged"] = EventCallback.Factory.Create<string>(this, v => _form.TagConfig = v),
|
["ConfigJsonChanged"] = EventCallback.Factory.Create<string>(this, v => _form.TagConfig = v),
|
||||||
["DriverType"] = DriverType ?? "",
|
["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()
|
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