fix(raw): manual tag entry feeds each row's picker the merged device config (#468 follow-up)
This commit is contained in:
+25
-3
@@ -1,7 +1,8 @@
|
||||
@* Bulk manual tag-entry grid for the v3 /raw tree: add MANY new tags at once under a Device or a
|
||||
TagGroup. Each row carries Name / DataType / AccessLevel / WriteIdempotent / PollGroup plus a
|
||||
per-row driver-typed TagConfig editor (revealed in an expandable full-width sub-row — the grid stays
|
||||
compact). The owning DriverType (fixed by the device) drives the typed-editor dispatch. On commit each
|
||||
compact), each fed the merged Driver+Device probe config so its address picker can browse the live
|
||||
device. The owning DriverType (fixed by the device) drives the typed-editor dispatch. On commit each
|
||||
row is created via CreateTagAsync; committed rows drop off, failed rows stay with their inline error so
|
||||
nothing is lost silently. Visibility is @bind-Visible; a successful commit raises OnSaved. *@
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@@ -159,11 +160,17 @@
|
||||
private bool _summaryOk;
|
||||
private bool _open; // guards re-seeding rows on unrelated re-renders
|
||||
|
||||
protected override void OnParametersSet()
|
||||
// Merged Driver+Device config for each row's address picker — the same blob Test-connect and
|
||||
// RawBrowseModal dial, resolved once per open; null (→ "{}") when the resolve failed: the picker's
|
||||
// browse degrades to its graceful error chip.
|
||||
private string? _mergedConfigJson;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (!Visible)
|
||||
{
|
||||
_open = false;
|
||||
_mergedConfigJson = null;
|
||||
return;
|
||||
}
|
||||
if (_open) { return; } // same open, re-render → preserve in-progress rows
|
||||
@@ -172,6 +179,21 @@
|
||||
_rows.Add(new RowModel());
|
||||
_summary = null;
|
||||
_summaryOk = false;
|
||||
|
||||
_mergedConfigJson = null;
|
||||
if (!string.IsNullOrEmpty(DeviceId))
|
||||
{
|
||||
try
|
||||
{
|
||||
var merged = await Svc.LoadMergedProbeConfigAsync(DeviceId);
|
||||
if (merged is { } m) { _mergedConfigJson = m.MergedConfigJson; }
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fail-soft: the grid must stay usable regardless — only the browse affordance inside each
|
||||
// row's picker degrades, and it reports its own error on use.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IDictionary<string, object> BuildEditorParameters(RowModel row) => new Dictionary<string, object>
|
||||
@@ -179,7 +201,7 @@
|
||||
["ConfigJson"] = row.TagConfig,
|
||||
["ConfigJsonChanged"] = EventCallback.Factory.Create<string>(this, v => row.TagConfig = v),
|
||||
["DriverType"] = DriverType,
|
||||
["GetDriverConfigJson"] = (Func<string>)(() => "{}"),
|
||||
["GetDriverConfigJson"] = (Func<string>)(() => _mergedConfigJson ?? "{}"),
|
||||
};
|
||||
|
||||
private void AddRow() => _rows.Add(new RowModel());
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
using System.Reflection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Raw;
|
||||
|
||||
/// <summary>
|
||||
/// Pins that the bulk manual-entry grid hands each row's typed tag editor the <b>merged</b>
|
||||
/// Driver+Device config, not a stub.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The AbCip / TwinCAT / FOCAS editors' address pickers dial the <c>GetDriverConfigJson</c> Func as
|
||||
/// their browse connect config. <see cref="RawManualTagEntryModal"/> passed a literal <c>"{}"</c>,
|
||||
/// so "Browse controller" always connected against an empty config and could never reach the
|
||||
/// device — the picker was authorable but unusable, and no offline test reached it (AdminUI has no
|
||||
/// bUnit).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Unlike the single-tag modal, the device here is a direct <c>[Parameter]</c> — no tag load
|
||||
/// precedes it — so the resolve keys off <c>DeviceId</c> on the open path. The accessor must stay
|
||||
/// <b>live</b> (reading the field, not a captured snapshot), because the resolve is awaited after
|
||||
/// the rows are seeded. Exercised as a plain class: AdminUI has no bUnit, so the injected service
|
||||
/// and the lifecycle/parameter-building members are reached by reflection.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class RawManualTagEntryModalMergedConfigTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Each_rows_editor_receives_the_merged_device_config_not_a_stub()
|
||||
{
|
||||
const string merged = """{"endpoint":"10.0.0.1:44818","slot":0}""";
|
||||
|
||||
var svc = new FakeRawTreeService
|
||||
{
|
||||
OnLoadMergedProbeConfig = deviceId =>
|
||||
deviceId == "dev-1" ? ("AbCip", merged) : null,
|
||||
};
|
||||
|
||||
var modal = new RawManualTagEntryModal();
|
||||
SetParameters(modal, visible: true, deviceId: "dev-1", driverType: "AbCip");
|
||||
InjectService(modal, svc);
|
||||
|
||||
await OpenAsync(modal);
|
||||
|
||||
GetDriverConfigJson(modal).ShouldBe(merged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_failed_resolve_degrades_to_the_empty_stub_and_does_not_throw()
|
||||
{
|
||||
var svc = new FakeRawTreeService
|
||||
{
|
||||
OnLoadMergedProbeConfig = _ => throw new InvalidOperationException("device unreachable"),
|
||||
};
|
||||
|
||||
var modal = new RawManualTagEntryModal();
|
||||
SetParameters(modal, visible: true, deviceId: "dev-1", driverType: "AbCip");
|
||||
InjectService(modal, svc);
|
||||
|
||||
await Should.NotThrowAsync(() => OpenAsync(modal));
|
||||
|
||||
GetDriverConfigJson(modal).ShouldBe("{}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the modal's <c>[Parameter]</c>s. By reflection because BL0005 forbids assigning component
|
||||
/// parameters from outside the component, and this project treats warnings as errors.
|
||||
/// </summary>
|
||||
private static void SetParameters(
|
||||
RawManualTagEntryModal modal, bool visible, string deviceId, string driverType)
|
||||
{
|
||||
Set("Visible", visible);
|
||||
Set("DeviceId", deviceId);
|
||||
Set("DriverType", driverType);
|
||||
|
||||
void Set(string name, object value)
|
||||
{
|
||||
var property = typeof(RawManualTagEntryModal).GetProperty(name);
|
||||
property.ShouldNotBeNull($"the [Parameter] {name} was renamed or removed.");
|
||||
property!.SetValue(modal, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sets the <c>@inject</c>-generated private <c>Svc</c> property.</summary>
|
||||
private static void InjectService(RawManualTagEntryModal modal, FakeRawTreeService svc)
|
||||
{
|
||||
var property = typeof(RawManualTagEntryModal).GetProperty(
|
||||
"Svc",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
property.ShouldNotBeNull("the injected IRawTreeService property is no longer named Svc.");
|
||||
|
||||
property!.SetValue(modal, svc);
|
||||
}
|
||||
|
||||
/// <summary>Drives the open path — the protected lifecycle method the resolve hangs off.</summary>
|
||||
private static async Task OpenAsync(RawManualTagEntryModal modal)
|
||||
{
|
||||
var method = typeof(RawManualTagEntryModal).GetMethod(
|
||||
"OnParametersSetAsync",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
method.ShouldNotBeNull("the open path moved off OnParametersSetAsync — retarget this driver.");
|
||||
|
||||
await (Task)method!.Invoke(modal, null)!;
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <c>GetDriverConfigJson</c> Func the first seeded row's editor is handed.</summary>
|
||||
private static string GetDriverConfigJson(RawManualTagEntryModal modal)
|
||||
{
|
||||
var rowsField = typeof(RawManualTagEntryModal).GetField(
|
||||
"_rows",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
rowsField.ShouldNotBeNull("the manual-entry grid's row list is no longer named _rows.");
|
||||
|
||||
var rows = ((System.Collections.IEnumerable)rowsField!.GetValue(modal)!).Cast<object>().ToList();
|
||||
rows.ShouldNotBeEmpty("the open path no longer seeds a row, so no editor parameters exist to check.");
|
||||
|
||||
var build = typeof(RawManualTagEntryModal).GetMethod(
|
||||
"BuildEditorParameters",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
build.ShouldNotBeNull("BuildEditorParameters was renamed — this test reads it by name.");
|
||||
|
||||
var parameters = (IDictionary<string, object>)build!.Invoke(modal, [rows[0]])!;
|
||||
|
||||
return ((Func<string>)parameters["GetDriverConfigJson"])();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user