feat(adminui): B2-WP6 browse re-target — /raw device browse commits raw tags
Browse device… on a /raw Device/TagGroup opens the discovery-browser modal against the merged Driver+Device config and commits selected browse leaves as raw Tag rows under the target, via IRawTreeService.ImportTagsAsync. - RawBrowseModal.razor: two-tier browse gate (bespoke IDriverBrowser → universal DiscoveryDriverBrowser via SupportsOnlineDiscovery → disabled), merged config from LoadMergedProbeConfigAsync, multi-select over DriverBrowseTree, opt-in 'create matching tag-groups' folder mirror, commit via ImportTagsAsync. - RawBrowseCommitMapper: pure leaf→RawTagImportRow mapper — DriverDataType→ OPC UA type, per-driver address field (nodeId/tagPath/symbolPath/address/ attributeRef), target-group + mirror path combine. Unit-tested (39 tests). - DriverBrowseTree: additive multi-select mode (checkboxes + ancestor-path callback), default off preserves single-select pickers. - RawTree: OnBrowseDevice wired to the modal (Device + TagGroup menus). Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
|
||||
/// <summary>
|
||||
/// Pure mapper for the WP6 "Browse device…" re-target: turns a selected driver-browse leaf into a
|
||||
/// <see cref="RawTagImportRow"/> ready for <see cref="IRawTreeService.ImportTagsAsync"/>. Under the v3
|
||||
/// identity contract the tag's identity is its RawPath (device + optional group + name), so the leaf's
|
||||
/// driver reference is written into the driver-typed <c>TagConfig</c> as an ordinary <b>address field</b>
|
||||
/// (<c>nodeId</c>/<c>tagPath</c>/<c>symbolPath</c>/<c>address</c>/<c>attributeRef</c> per driver) — never an
|
||||
/// identity key. The address-field write reuses the same <c><Driver>TagConfigModel</c> the typed tag
|
||||
/// editors use, so a browse-committed tag round-trips through that editor unchanged.
|
||||
/// </summary>
|
||||
public static class RawBrowseCommitMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps one selected browse leaf into an import row.
|
||||
/// </summary>
|
||||
/// <param name="driverType">The owning device's driver type (a <see cref="DriverTypeNames"/> value).</param>
|
||||
/// <param name="fullName">The leaf's driver-side full reference (the <c>BrowseNode.NodeId</c> = the
|
||||
/// captured <c>DriverAttributeInfo.FullName</c>) — becomes the driver-typed address field.</param>
|
||||
/// <param name="browseName">The leaf's browse name — becomes the raw tag <c>Name</c> (a RawPath segment).</param>
|
||||
/// <param name="driverDataType">The leaf's <see cref="DriverDataType"/> name (from the browse
|
||||
/// attribute side-channel), or null when the browser cannot report a type (e.g. the OPC UA Client tree).</param>
|
||||
/// <param name="defaultDataType">The OPC-UA built-in type name to fall back to when
|
||||
/// <paramref name="driverDataType"/> is null/unmappable.</param>
|
||||
/// <param name="groupPrefix">The target TagGroup's device-relative path when committing under a group,
|
||||
/// or null when committing at the device root. Prepended to any mirrored folder path.</param>
|
||||
/// <param name="folderPath">The captured browse-folder nesting above the leaf (root→parent display
|
||||
/// names); mirrored onto nested TagGroups only when <paramref name="createGroups"/> is true.</param>
|
||||
/// <param name="createGroups">When true, mirror <paramref name="folderPath"/> as nested TagGroups.</param>
|
||||
/// <returns>The assembled import row.</returns>
|
||||
public static RawTagImportRow MapLeaf(
|
||||
string driverType,
|
||||
string fullName,
|
||||
string browseName,
|
||||
string? driverDataType,
|
||||
string defaultDataType,
|
||||
string? groupPrefix,
|
||||
IReadOnlyList<string>? folderPath,
|
||||
bool createGroups)
|
||||
{
|
||||
var dataType = MapDataType(driverDataType) ?? defaultDataType;
|
||||
var tagConfig = BuildTagConfig(driverType, fullName);
|
||||
var mirrored = createGroups && folderPath is { Count: > 0 }
|
||||
? string.Join(RawPaths.Separator, folderPath)
|
||||
: null;
|
||||
var groupPath = CombineGroupPath(groupPrefix, mirrored);
|
||||
|
||||
// Access baseline mirrors manual entry (Read); write-idempotent stays false (unknown at browse time);
|
||||
// no poll-group (batching is authored later).
|
||||
return new RawTagImportRow(
|
||||
groupPath,
|
||||
new RawTagInput(browseName, dataType, TagAccessLevel.Read, WriteIdempotent: false, PollGroupId: null, tagConfig));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="DriverDataType"/> enum NAME (as reported by the browse attribute side-channel) to the
|
||||
/// OPC-UA built-in type name a <see cref="RawTagInput.DataType"/> carries. Mirrors
|
||||
/// <c>DiscoveredNodeMapper.ToBuiltinTypeString</c>: most names pass through, <c>Float32</c>/<c>Float64</c>
|
||||
/// become <c>Float</c>/<c>Double</c>, and <c>Reference</c> (a Galaxy attribute ref) carries as <c>String</c>.
|
||||
/// Returns null when the input is null/blank or not a known driver data type (caller supplies a default).
|
||||
/// </summary>
|
||||
/// <param name="driverDataType">The <see cref="DriverDataType"/> name, or null/blank.</param>
|
||||
/// <returns>The OPC-UA built-in type name, or null when unmappable.</returns>
|
||||
public static string? MapDataType(string? driverDataType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(driverDataType)
|
||||
|| !Enum.TryParse<DriverDataType>(driverDataType, ignoreCase: true, out var dt))
|
||||
return null;
|
||||
|
||||
return dt switch
|
||||
{
|
||||
DriverDataType.Boolean => "Boolean",
|
||||
DriverDataType.Int16 => "Int16",
|
||||
DriverDataType.Int32 => "Int32",
|
||||
DriverDataType.Int64 => "Int64",
|
||||
DriverDataType.UInt16 => "UInt16",
|
||||
DriverDataType.UInt32 => "UInt32",
|
||||
DriverDataType.UInt64 => "UInt64",
|
||||
DriverDataType.Float32 => "Float",
|
||||
DriverDataType.Float64 => "Double",
|
||||
DriverDataType.String => "String",
|
||||
DriverDataType.DateTime => "DateTime",
|
||||
DriverDataType.Reference => "String",
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the driver-typed <c>TagConfig</c> JSON for a browse-committed tag, setting ONLY the driver's
|
||||
/// address field to <paramref name="fullName"/> and leaving every other field at its model default. Reuses
|
||||
/// the <c><Driver>TagConfigModel</c> for driver types that have a typed editor; the model-less Galaxy
|
||||
/// driver gets the canonical camelCase <c>attributeRef</c> key directly.
|
||||
/// </summary>
|
||||
/// <param name="driverType">The owning device's driver type.</param>
|
||||
/// <param name="fullName">The leaf's driver-side full reference to write into the address field.</param>
|
||||
/// <returns>The serialised driver-typed <c>TagConfig</c> JSON.</returns>
|
||||
public static string BuildTagConfig(string driverType, string fullName)
|
||||
{
|
||||
var address = fullName ?? "";
|
||||
if (Is(driverType, DriverTypeNames.OpcUaClient))
|
||||
return new OpcUaClientTagConfigModel { NodeId = address }.ToJson();
|
||||
if (Is(driverType, DriverTypeNames.AbCip))
|
||||
return new AbCipTagConfigModel { TagPath = address }.ToJson();
|
||||
if (Is(driverType, DriverTypeNames.AbLegacy))
|
||||
return new AbLegacyTagConfigModel { Address = address }.ToJson();
|
||||
if (Is(driverType, DriverTypeNames.TwinCAT))
|
||||
return new TwinCATTagConfigModel { SymbolPath = address }.ToJson();
|
||||
if (Is(driverType, DriverTypeNames.FOCAS))
|
||||
return new FocasTagConfigModel { Address = address }.ToJson();
|
||||
if (Is(driverType, DriverTypeNames.S7))
|
||||
return new S7TagConfigModel { Address = address }.ToJson();
|
||||
if (Is(driverType, DriverTypeNames.Galaxy))
|
||||
return WriteSingleKey("attributeRef", address);
|
||||
|
||||
// Unknown/flat-address driver (e.g. Modbus is not browsable): record the reference under a generic
|
||||
// "address" key so nothing is lost. Browsable drivers are all handled above.
|
||||
return WriteSingleKey("address", address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines a target-group path prefix with an optional mirrored sub-path, matching the WP5 CSV import
|
||||
/// combine semantics: blank collapses to null, and a present prefix + suffix join with the RawPath
|
||||
/// separator. Returns null when both are absent (⇒ commit at the device root).
|
||||
/// </summary>
|
||||
/// <param name="prefix">The target TagGroup's device-relative path, or null/blank for the device root.</param>
|
||||
/// <param name="suffix">The mirrored folder sub-path, or null/blank.</param>
|
||||
/// <returns>The combined device-relative group path, or null for the device root.</returns>
|
||||
public static string? CombineGroupPath(string? prefix, string? suffix)
|
||||
{
|
||||
prefix = string.IsNullOrWhiteSpace(prefix) ? null : prefix.Trim();
|
||||
suffix = string.IsNullOrWhiteSpace(suffix) ? null : suffix.Trim();
|
||||
if (prefix is null) return suffix;
|
||||
return suffix is null ? prefix : $"{prefix}{RawPaths.SeparatorString}{suffix}";
|
||||
}
|
||||
|
||||
private static bool Is(string driverType, string name)
|
||||
=> string.Equals(driverType, name, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string WriteSingleKey(string key, string value)
|
||||
{
|
||||
var o = new JsonObject { [key] = value };
|
||||
return o.ToJsonString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user