contracts(b2-waveA): RawNode view-model + IRawTreeService read surface + RawRenameResult
Wave A contract-first fan-out types for the /raw project tree: - RawNode/RawNodeKind: shared tree view-model (Enterprise→Cluster→Folder→Driver→Device→TagGroup→Tag), lazy-load metadata, per-kind ids + DriverType propagation for menu gating. - IRawTreeService: committed READ surface (LoadRootsAsync + LoadChildrenAsync); WP1 extends with the mutation surface, WP2 consumes read-only. - RawRenameResult: rename outcome carrying non-blocking warnings (historized/UNS-referenced now; script-scan in Batch 3). Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and mutates the v3 Raw project tree (<c>/raw</c>) — the cluster-rooted
|
||||
/// Enterprise → Cluster → Folder → Driver → Device → TagGroup → Tag hierarchy — from the config
|
||||
/// database. The peer of <see cref="IUnsTreeService"/> for the Raw subtree.
|
||||
/// <para>
|
||||
/// <b>Contract split (Batch 2 Wave A):</b> this file's committed surface is the <b>read</b> contract
|
||||
/// the <c>RawTree</c> component consumes — <see cref="LoadRootsAsync"/> (eager Enterprise→Cluster roots)
|
||||
/// and <see cref="LoadChildrenAsync"/> (one lazy level per expand). B2-WP1 (<c>RawTreeService</c>)
|
||||
/// <b>extends this interface</b> with the mutation surface — create/rename/delete per node type,
|
||||
/// move-into-folder, and the per-node edit projections — enforcing the integrity rules with
|
||||
/// user-readable errors (a delete blocked by a <c>UnsTagReference</c> names the referencing equipment).
|
||||
/// Rename methods return <see cref="RawRenameResult"/> so the UI can show non-blocking warnings.
|
||||
/// Mutations reuse <see cref="UnsMutationResult"/>. WP2 must not add to this interface.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface IRawTreeService
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads the eager top of the Raw tree: Enterprise → Cluster roots, each cluster marked
|
||||
/// <see cref="RawNode.HasLazyChildren"/> so its folders + drivers load on first expand. Empty
|
||||
/// clusters are retained so they stay visible and authorable. The returned nodes are detached
|
||||
/// view-models, safe to hold and mutate UI state on after the underlying context is disposed.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token to cancel the load.</param>
|
||||
/// <returns>The enterprise roots, populated down to (but not through) their clusters.</returns>
|
||||
Task<IReadOnlyList<RawNode>> LoadRootsAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the direct children of one container node — the next level down, keyed off
|
||||
/// <paramref name="parent"/>'s <see cref="RawNode.Kind"/> and <see cref="RawNode.EntityId"/>:
|
||||
/// a Cluster yields its root Folders + Drivers; a Folder yields sub-Folders + Drivers; a Driver
|
||||
/// yields its Devices; a Device yields its root TagGroups + Tags; a TagGroup yields sub-TagGroups
|
||||
/// + Tags. Tags are leaves and yield nothing. Ordering is deterministic and ordinal. Reads
|
||||
/// untracked; returns detached nodes. Per-device tag counts can be large — the implementation may
|
||||
/// page, but the committed shape returns the level's nodes (paging knobs, if any, are WP1's to add
|
||||
/// on the concrete type without changing this signature's meaning for the tree).
|
||||
/// </summary>
|
||||
/// <param name="parent">The container node whose direct children to load.</param>
|
||||
/// <param name="ct">A token to cancel the load.</param>
|
||||
/// <returns>The parent's direct child nodes; empty for a leaf or an empty container.</returns>
|
||||
Task<IReadOnlyList<RawNode>> LoadChildrenAsync(RawNode parent, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
|
||||
/// <summary>
|
||||
/// The kind of node within the v3 Raw project tree (<c>/raw</c>). Determines how the
|
||||
/// renderer styles the row, which context-menu it shows, and which entity (if any) it
|
||||
/// links to. The Raw tree is cluster-rooted and mirrors the RawPath hierarchy:
|
||||
/// Enterprise → Cluster → Folder(s) → Driver → Device → TagGroup(s) → Tag.
|
||||
/// </summary>
|
||||
public enum RawNodeKind
|
||||
{
|
||||
/// <summary>Top-level grouping label (a string on cluster rows, not a DB entity).</summary>
|
||||
Enterprise,
|
||||
|
||||
/// <summary>A server cluster — the root of a Raw project tree.</summary>
|
||||
Cluster,
|
||||
|
||||
/// <summary>A driver-organizing <c>RawFolder</c> (nestable under a cluster or another folder).</summary>
|
||||
Folder,
|
||||
|
||||
/// <summary>A <c>DriverInstance</c>.</summary>
|
||||
Driver,
|
||||
|
||||
/// <summary>A <c>Device</c> under a driver (universal in v3 — every driver has ≥1).</summary>
|
||||
Device,
|
||||
|
||||
/// <summary>A tag-organizing <c>TagGroup</c> (nestable under a device or another group).</summary>
|
||||
TagGroup,
|
||||
|
||||
/// <summary>A raw <c>Tag</c> (signal) — a leaf.</summary>
|
||||
Tag,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View-model for a single node in the Raw project tree. Carries the stable identity,
|
||||
/// display text, per-kind ids, and lazy-load metadata the renderer needs, plus transient UI
|
||||
/// state (expansion/loading) that is never persisted. Detached from EF — safe to hold and
|
||||
/// mutate UI state on after the loading context is disposed. This is the shared contract
|
||||
/// between <see cref="IRawTreeService"/> (which produces nodes) and the <c>RawTree</c>
|
||||
/// component (which renders + lazily expands them).
|
||||
/// </summary>
|
||||
public sealed class RawNode
|
||||
{
|
||||
/// <summary>The kind of node — drives styling, context-menu, and entity linking.</summary>
|
||||
public required RawNodeKind Kind { get; init; }
|
||||
|
||||
/// <summary>Stable per-node key, unique within the tree (e.g. <c>drv:{driverInstanceId}</c>).</summary>
|
||||
public required string Key { get; init; }
|
||||
|
||||
/// <summary>Human-readable label shown in the tree (the node's <c>Name</c>, or Enterprise/Cluster label).</summary>
|
||||
public required string DisplayName { get; init; }
|
||||
|
||||
/// <summary>Owning cluster id for every node under a cluster (and the cluster itself); null for Enterprise.</summary>
|
||||
public string? ClusterId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// This node's own logical entity id (<c>RawFolderId</c>/<c>DriverInstanceId</c>/<c>DeviceId</c>/
|
||||
/// <c>TagGroupId</c>/<c>TagId</c>, or the <c>ClusterId</c> for a cluster); null for Enterprise. The
|
||||
/// child loader keys off <see cref="Kind"/> + this id to fetch the next level.
|
||||
/// </summary>
|
||||
public string? EntityId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The bound <c>DriverType</c> for <see cref="RawNodeKind.Driver"/> (and propagated to Device/TagGroup/Tag
|
||||
/// nodes beneath it, so tag editors and the browse-enable gate can dispatch without another query);
|
||||
/// null for folders/clusters/enterprise. Use the <c>DriverTypeNames</c> constants when matching.
|
||||
/// </summary>
|
||||
public string? DriverType { get; init; }
|
||||
|
||||
/// <summary>Whether a Driver/Device node is enabled (drives the enable/disable menu label + styling); true otherwise.</summary>
|
||||
public bool Enabled { get; init; } = true;
|
||||
|
||||
/// <summary>Direct child count (for the badge). For a device this is its tag-group + tag count.</summary>
|
||||
public int ChildCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this node's children load lazily on expand. True for every container kind in the Raw tree
|
||||
/// (Cluster/Folder/Driver/Device/TagGroup) — unlike the UNS tree, <c>/raw</c> actually exercises the
|
||||
/// lazy plumbing (per-device tag counts can be large). Tags are leaves (false).
|
||||
/// </summary>
|
||||
public bool HasLazyChildren { get; init; }
|
||||
|
||||
/// <summary>Lazily-materialised children — empty until <see cref="Loaded"/> flips on expand.</summary>
|
||||
public List<RawNode> Children { get; } = new();
|
||||
|
||||
/// <summary>Optimistic-concurrency token for this node's backing row (empty for Enterprise); echoed on mutations.</summary>
|
||||
public byte[] RowVersion { get; init; } = Array.Empty<byte>();
|
||||
|
||||
// --- Runtime UI state (not persisted) ---
|
||||
|
||||
/// <summary>Whether the node is currently expanded in the UI.</summary>
|
||||
public bool Expanded { get; set; }
|
||||
|
||||
/// <summary>Whether the node's lazy children have been loaded.</summary>
|
||||
public bool Loaded { get; set; }
|
||||
|
||||
/// <summary>Whether a lazy-load is currently in flight for this node.</summary>
|
||||
public bool Loading { get; set; }
|
||||
|
||||
/// <summary>Last load error message, if any.</summary>
|
||||
public string? Error { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
|
||||
/// <summary>
|
||||
/// Outcome of a Raw-tree rename. Like <see cref="UnsMutationResult"/> it reports success/failure,
|
||||
/// but a rename additionally surfaces <see cref="Warnings"/>: non-blocking advisories the UI shows
|
||||
/// after the rename succeeds (a RawPath segment changed, so downstream references may need attention).
|
||||
/// In this batch the warnings answerable from the schema are wired — a renamed node with historized
|
||||
/// tags beneath it, or raw tags referenced by an equipment (<c>UnsTagReference</c>). Batch 3 adds the
|
||||
/// script-substring scan to the same list. On failure <see cref="Ok"/> is false, <see cref="Error"/>
|
||||
/// carries the operator-facing message, and <see cref="Warnings"/> is empty.
|
||||
/// </summary>
|
||||
/// <param name="Ok">Whether the rename was applied.</param>
|
||||
/// <param name="Error">The operator-facing failure message, or <c>null</c> on success.</param>
|
||||
/// <param name="Warnings">Non-blocking advisories to show after a successful rename; empty when there are none.</param>
|
||||
public readonly record struct RawRenameResult(bool Ok, string? Error, IReadOnlyList<string> Warnings)
|
||||
{
|
||||
/// <summary>A successful rename carrying zero or more advisory warnings.</summary>
|
||||
public static RawRenameResult Success(IReadOnlyList<string>? warnings = null) =>
|
||||
new(true, null, warnings ?? Array.Empty<string>());
|
||||
|
||||
/// <summary>A failed rename carrying the operator-facing message.</summary>
|
||||
public static RawRenameResult Failure(string error) =>
|
||||
new(false, error, Array.Empty<string>());
|
||||
}
|
||||
Reference in New Issue
Block a user