77bc010ba9
Replace the Batch-1-stubbed authored-tag flow with a UnsTagReference list on the equipment page's Tags tab (v3 reference-only equipment): columns are effective name, computed raw path, inherited datatype/access (read-only), display-name override (editable), and remove. The retired equipment-authored-tag editor (TagModal.razor) is deleted; the VirtualTag and ScriptedAlarm flows are untouched. - "+ Add reference" opens a new AddReferenceModal that reuses RawTree in a new opt-in PickerMode (Tag-leaf checkboxes + Device/TagGroup "select all tags below"), scoped structurally to the equipment's cluster via LoadReferencePickerRootAsync (a single cluster root — cross-cluster tags are unreachable, not merely hidden). Default /raw usage is byte-unchanged (PickerMode defaults false). - UnsTreeService (+ IUnsTreeService) gain the reference mutations: LoadReferencesForEquipmentAsync (computes each RawPath via the shared RawPathResolver), AddReferencesAsync (cluster-checked, all-or-nothing), RemoveReferenceAsync, SetReferenceOverrideAsync, plus the picker helpers LoadReferencePickerRootAsync / LoadDescendantTagIdsAsync. - Consume IEffectiveNameGuard (constructor-injected, no-op fallback when unregistered): AddReferences, SetReferenceOverride, and the VirtualTag + ScriptedAlarm create/update mutations now call CheckAsync before persisting and surface a collision as the failure. WP2 supplies the implementation + DI registration. - Drop the retired equipment↔driver binding: remove DriverInstanceId from EquipmentInput, the ImportEquipmentModal CSV column, the EquipmentPage Details driver select, and the dead decision-#122 driver-cluster guard. Tests: new UnsTreeServiceReferenceTests (add/remove/override, cross-cluster + duplicate rejection, guard-consumed rejection via a fake guard, RawPath projection, picker helpers); Equipment/Import test suites rewritten to drop the retired driver-guard cases. AdminUI suite green (639 passed, 3 skipped); full solution builds 0 errors. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
633 lines
43 KiB
C#
633 lines
43 KiB
C#
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
|
|
|
/// <summary>
|
|
/// A UNS area projected for editing: its operator-editable fields plus the owning cluster and
|
|
/// the concurrency token the edit modal must echo back on save.
|
|
/// </summary>
|
|
/// <param name="UnsAreaId">The area's stable id (read-only on edit).</param>
|
|
/// <param name="Name">The area name.</param>
|
|
/// <param name="Notes">Optional notes; <c>null</c> when unset.</param>
|
|
/// <param name="ClusterId">The owning cluster id (the served-by selection).</param>
|
|
/// <param name="RowVersion">The optimistic-concurrency token last read.</param>
|
|
public sealed record AreaEditDto(string UnsAreaId, string Name, string? Notes, string ClusterId, byte[] RowVersion);
|
|
|
|
/// <summary>
|
|
/// A UNS line projected for editing: its operator-editable fields plus the parent area and the
|
|
/// concurrency token the edit modal must echo back on save.
|
|
/// </summary>
|
|
/// <param name="UnsLineId">The line's stable id (read-only on edit).</param>
|
|
/// <param name="UnsAreaId">The owning area id (the parent-area selection).</param>
|
|
/// <param name="Name">The line name.</param>
|
|
/// <param name="Notes">Optional notes; <c>null</c> when unset.</param>
|
|
/// <param name="RowVersion">The optimistic-concurrency token last read.</param>
|
|
public sealed record LineEditDto(string UnsLineId, string UnsAreaId, string Name, string? Notes, byte[] RowVersion);
|
|
|
|
/// <summary>
|
|
/// An equipment projected for editing: its system-generated id, the operator-editable identity and
|
|
/// OPC 40010 identification fields, plus the concurrency token the edit modal must echo back on save.
|
|
/// </summary>
|
|
/// <param name="EquipmentId">The system-generated stable id (read-only — never operator-edited).</param>
|
|
/// <param name="Name">UNS level-5 segment name.</param>
|
|
/// <param name="MachineCode">Operator colloquial id; unique fleet-wide.</param>
|
|
/// <param name="UnsLineId">The owning line id (the UNS-line selection).</param>
|
|
/// <param name="DriverInstanceId">Optional driver binding; <c>null</c> when driver-less.</param>
|
|
/// <param name="ZTag">Optional ERP equipment id.</param>
|
|
/// <param name="SAPID">Optional SAP PM equipment id.</param>
|
|
/// <param name="Manufacturer">Optional OPC 40010 manufacturer name.</param>
|
|
/// <param name="Model">Optional OPC 40010 model designation.</param>
|
|
/// <param name="SerialNumber">Optional OPC 40010 serial number.</param>
|
|
/// <param name="HardwareRevision">Optional OPC 40010 hardware revision.</param>
|
|
/// <param name="SoftwareRevision">Optional OPC 40010 software revision.</param>
|
|
/// <param name="YearOfConstruction">Optional OPC 40010 year of construction.</param>
|
|
/// <param name="AssetLocation">Optional OPC 40010 asset location.</param>
|
|
/// <param name="ManufacturerUri">Optional OPC 40010 manufacturer URI.</param>
|
|
/// <param name="DeviceManualUri">Optional OPC 40010 device-manual URI.</param>
|
|
/// <param name="Enabled">Whether the equipment is surfaced in deployments.</param>
|
|
/// <param name="RowVersion">The optimistic-concurrency token last read.</param>
|
|
public sealed record EquipmentEditDto(string EquipmentId, string Name, string MachineCode, string UnsLineId,
|
|
string? DriverInstanceId, string? ZTag, string? SAPID, string? Manufacturer, string? Model, string? SerialNumber,
|
|
string? HardwareRevision, string? SoftwareRevision, short? YearOfConstruction, string? AssetLocation,
|
|
string? ManufacturerUri, string? DeviceManualUri, bool Enabled, byte[] RowVersion);
|
|
|
|
/// <summary>
|
|
/// An equipment-bound tag projected for editing: its operator-editable fields, the owning equipment
|
|
/// (so the host can scope the candidate-driver list and refresh the right node), plus the concurrency
|
|
/// token the edit modal must echo back on save. Tree tags are always equipment-bound,
|
|
/// so <c>FolderPath</c> never surfaces here.
|
|
/// </summary>
|
|
/// <param name="TagId">The tag's stable id (read-only on edit).</param>
|
|
/// <param name="EquipmentId">The owning equipment id.</param>
|
|
/// <param name="Name">The tag name.</param>
|
|
/// <param name="DriverInstanceId">The bound driver id.</param>
|
|
/// <param name="DataType">The OPC UA built-in type name.</param>
|
|
/// <param name="AccessLevel">The tag-level access baseline.</param>
|
|
/// <param name="WriteIdempotent">Whether writes are safe to retry.</param>
|
|
/// <param name="PollGroupId">Optional poll-group key; <c>null</c> when unset.</param>
|
|
/// <param name="TagConfig">The schemaless per-driver-type JSON config.</param>
|
|
/// <param name="RowVersion">The optimistic-concurrency token last read.</param>
|
|
public sealed record TagEditDto(string TagId, string EquipmentId, string Name, string DriverInstanceId, string DataType,
|
|
TagAccessLevel AccessLevel, bool WriteIdempotent, string? PollGroupId, string TagConfig, byte[] RowVersion);
|
|
|
|
/// <summary>
|
|
/// An equipment-bound virtual tag projected for editing: its operator-editable fields, the owning
|
|
/// equipment (so the host can refresh the right node), plus the concurrency token the edit modal must
|
|
/// echo back on save. Virtual tags are always scoped to an equipment, so the modal
|
|
/// never offers an equipment-change control.
|
|
/// </summary>
|
|
/// <param name="VirtualTagId">The virtual tag's stable id (read-only on edit).</param>
|
|
/// <param name="EquipmentId">The owning equipment id.</param>
|
|
/// <param name="Name">The virtual-tag name.</param>
|
|
/// <param name="DataType">The OPC UA built-in type name.</param>
|
|
/// <param name="ScriptId">The bound script id.</param>
|
|
/// <param name="ChangeTriggered">Whether the tag re-evaluates on dependency change.</param>
|
|
/// <param name="TimerIntervalMs">Optional periodic re-evaluation cadence in ms; <c>null</c> when unset.</param>
|
|
/// <param name="Historize">Whether the tag's values are historized.</param>
|
|
/// <param name="Enabled">Whether the tag is spawned in deployments.</param>
|
|
/// <param name="RowVersion">The optimistic-concurrency token last read.</param>
|
|
public sealed record VirtualTagEditDto(string VirtualTagId, string EquipmentId, string Name, string DataType, string ScriptId,
|
|
bool ChangeTriggered, int? TimerIntervalMs, bool Historize, bool Enabled, byte[] RowVersion);
|
|
|
|
/// <summary>
|
|
/// The outcome of a bulk equipment CSV import: how many rows were inserted, how many were skipped
|
|
/// (existing MachineCode — the importer is additive-only, never an update), and a per-row error list
|
|
/// for rows that could not be inserted (unknown line, unknown driver, or a cluster
|
|
/// mismatch). Skipped rows never appear in <see cref="Errors"/>.
|
|
/// </summary>
|
|
/// <param name="Inserted">The count of new Equipment rows added.</param>
|
|
/// <param name="Skipped">The count of rows skipped because their MachineCode already exists.</param>
|
|
/// <param name="Errors">The human-readable error strings for rows that failed validation.</param>
|
|
public sealed record EquipmentImportResult(int Inserted, int Skipped, IReadOnlyList<string> Errors);
|
|
|
|
/// <summary>
|
|
/// Loads the structural portion of the unified-namespace (UNS) browse tree —
|
|
/// Enterprise → Cluster → Area → Line → Equipment — from the config database.
|
|
/// Equipment is a leaf in the tree; its tags and virtual tags are accessed via the
|
|
/// equipment detail page, not via lazy tree expansion.
|
|
/// </summary>
|
|
public interface IUnsTreeService
|
|
{
|
|
/// <summary>
|
|
/// Loads the full structural tree. Empty clusters are retained so they remain
|
|
/// visible and editable. 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 root nodes, each populated down to equipment.</returns>
|
|
Task<IReadOnlyList<UnsNode>> LoadStructureAsync(CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Loads the driver tags bound to a single equipment as flat row projections for the equipment
|
|
/// page's Tags tab table, ordered by Name. Each row carries the display columns plus the
|
|
/// <c>TagId</c> the table uses to open the edit modal. Reads untracked. Returns an empty list when
|
|
/// the equipment has no tags.
|
|
/// </summary>
|
|
/// <param name="equipmentId">The equipment whose tags to load.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The equipment's tag rows ordered by Name; empty if it has none.</returns>
|
|
Task<IReadOnlyList<EquipmentTagRow>> LoadTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Loads the virtual tags scoped to a single equipment as flat row projections for the equipment
|
|
/// page's Virtual Tags tab table, ordered by Name. Each row carries the display columns plus the
|
|
/// <c>VirtualTagId</c> the table uses to open the edit modal. Reads untracked. Returns an empty list
|
|
/// when the equipment has no virtual tags.
|
|
/// </summary>
|
|
/// <param name="equipmentId">The equipment whose virtual tags to load.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The equipment's virtual-tag rows ordered by Name; empty if it has none.</returns>
|
|
Task<IReadOnlyList<EquipmentVirtualTagRow>> LoadVirtualTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default);
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// v3 UNS reference-only equipment: the Tags tab lists UnsTagReference rows pointing at raw tags.
|
|
// ---------------------------------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Loads the <see cref="Configuration.Entities.UnsTagReference"/> rows for a single equipment as flat
|
|
/// projections for the equipment page's Tags tab, ordered by <c>SortOrder</c> then effective name.
|
|
/// Each row carries the effective name (override else the backing raw tag's <c>Name</c>), the backing
|
|
/// tag's computed RawPath, its inherited (read-only) datatype/access, the optional display-name
|
|
/// override, and the reference's concurrency token. Reads untracked. Returns an empty list when the
|
|
/// equipment has no references.
|
|
/// </summary>
|
|
/// <param name="equipmentId">The equipment whose references to load.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The equipment's reference rows; empty if it has none.</returns>
|
|
Task<IReadOnlyList<EquipmentReferenceRow>> LoadReferencesForEquipmentAsync(string equipmentId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Adds one <see cref="Configuration.Entities.UnsTagReference"/> per supplied raw <c>TagId</c> to an
|
|
/// equipment. Every backing tag MUST live in the same cluster as the equipment (cross-cluster
|
|
/// references are rejected). Each new reference's effective name (the raw tag's <c>Name</c> — no
|
|
/// override on add) must be unique within the equipment across references, VirtualTags, and
|
|
/// ScriptedAlarms (enforced via <see cref="IEffectiveNameGuard"/>), and a tag already referenced by
|
|
/// the equipment is rejected. The batch is all-or-nothing.
|
|
/// </summary>
|
|
/// <param name="equipmentId">The referencing equipment.</param>
|
|
/// <param name="tagIds">The raw tag ids to reference.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, or the first guard/cluster/duplicate failure (nothing is added).</returns>
|
|
Task<UnsMutationResult> AddReferencesAsync(string equipmentId, IReadOnlyList<string> tagIds, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Removes a UNS tag reference. A missing row is treated as success (already gone). Uses last-write-wins
|
|
/// optimistic concurrency on <see cref="Configuration.Entities.UnsTagReference.RowVersion"/>.
|
|
/// </summary>
|
|
/// <param name="unsTagReferenceId">The reference to remove.</param>
|
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a concurrency failure, or a delete-failed failure.</returns>
|
|
Task<UnsMutationResult> RemoveReferenceAsync(string unsTagReferenceId, byte[] rowVersion, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Sets (or clears) a reference's <c>DisplayNameOverride</c>. A whitespace-only override collapses to
|
|
/// <c>null</c> (use the raw name). The resulting effective name (override else the backing raw tag's
|
|
/// <c>Name</c>) must be unique within the equipment across references, VirtualTags, and ScriptedAlarms
|
|
/// (enforced via <see cref="IEffectiveNameGuard"/>, excluding this reference). Uses last-write-wins
|
|
/// optimistic concurrency.
|
|
/// </summary>
|
|
/// <param name="unsTagReferenceId">The reference to update.</param>
|
|
/// <param name="displayNameOverride">The new override, or <c>null</c>/blank to clear it.</param>
|
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a missing-row failure, a name-collision failure, or a concurrency failure.</returns>
|
|
Task<UnsMutationResult> SetReferenceOverrideAsync(string unsTagReferenceId, string? displayNameOverride, byte[] rowVersion, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Builds the single cluster-rooted <see cref="RawNode"/> that seeds the "+ Add reference" raw-tag
|
|
/// picker for an equipment, structurally scoped to the equipment's own cluster (raw tags in any other
|
|
/// cluster are unreachable through this root — the scope is enforced by the query, not merely hidden).
|
|
/// The picker's <c>RawTree</c> lazily expands it via <see cref="IRawTreeService.LoadChildrenAsync"/>.
|
|
/// Returns <c>null</c> when the equipment cannot be resolved to a cluster.
|
|
/// </summary>
|
|
/// <param name="equipmentId">The equipment whose cluster scopes the picker.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The cluster root node, or <c>null</c> when the equipment/cluster can't be resolved.</returns>
|
|
Task<RawNode?> LoadReferencePickerRootAsync(string equipmentId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Enumerates every raw <c>TagId</c> beneath a Device or TagGroup picker node (the whole subtree), for
|
|
/// the picker's "select all tags below" affordance. Returns an empty list for non-container node kinds.
|
|
/// </summary>
|
|
/// <param name="node">The picker node (Device or TagGroup) to enumerate tags under.</param>
|
|
/// <param name="ct">A token to cancel the query.</param>
|
|
/// <returns>The tag ids in the node's subtree; empty for unsupported kinds.</returns>
|
|
Task<IReadOnlyList<string>> LoadDescendantTagIdsAsync(RawNode node, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Loads a single UNS area projected for editing, or <c>null</c> if it no longer exists.
|
|
/// Reads untracked and captures the current concurrency token for last-write-wins saves.
|
|
/// </summary>
|
|
/// <param name="unsAreaId">The area to load.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The area's edit projection, or <c>null</c> when missing.</returns>
|
|
Task<AreaEditDto?> LoadAreaAsync(string unsAreaId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Loads a single UNS line projected for editing, or <c>null</c> if it no longer exists.
|
|
/// Reads untracked and captures the current concurrency token for last-write-wins saves.
|
|
/// </summary>
|
|
/// <param name="unsLineId">The line to load.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The line's edit projection, or <c>null</c> when missing.</returns>
|
|
Task<LineEditDto?> LoadLineAsync(string unsLineId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Loads a single equipment projected for editing, or <c>null</c> if it no longer exists.
|
|
/// Reads untracked and captures the current concurrency token for last-write-wins saves.
|
|
/// </summary>
|
|
/// <param name="equipmentId">The equipment to load.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The equipment's edit projection, or <c>null</c> when missing.</returns>
|
|
Task<EquipmentEditDto?> LoadEquipmentAsync(string equipmentId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Loads a single equipment-bound tag projected for editing, or <c>null</c> if it no longer exists.
|
|
/// Reads untracked and captures the current concurrency token for last-write-wins saves.
|
|
/// </summary>
|
|
/// <param name="tagId">The tag to load.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The tag's edit projection, or <c>null</c> when missing.</returns>
|
|
Task<TagEditDto?> LoadTagAsync(string tagId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Loads a single equipment-bound virtual tag projected for editing, or <c>null</c> if it no longer
|
|
/// exists. Reads untracked and captures the current concurrency token for last-write-wins saves.
|
|
/// </summary>
|
|
/// <param name="virtualTagId">The virtual tag to load.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The virtual tag's edit projection, or <c>null</c> when missing.</returns>
|
|
Task<VirtualTagEditDto?> LoadVirtualTagAsync(string virtualTagId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Loads every driver instance in a cluster (regardless of namespace kind) so the equipment modal
|
|
/// can offer the full cluster driver list for binding. Ordered by <c>DriverInstanceId</c>. Each is
|
|
/// projected to a <c>(DriverInstanceId, Display)</c> pair where <c>Display</c> is
|
|
/// <c>"{DriverInstanceId} — {Name} ({DriverType})"</c>.
|
|
/// </summary>
|
|
/// <param name="clusterId">The cluster whose drivers to load.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The cluster's drivers projected to <c>(DriverInstanceId, Display)</c> pairs.</returns>
|
|
Task<IReadOnlyList<(string DriverInstanceId, string Display)>> LoadDriversForClusterAsync(string clusterId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Loads the UNS-line and driver <c>(Id, Display)</c> option lists the equipment page's Details tab
|
|
/// offers, scoped to the cluster that owns the supplied line: every line in that cluster (for the
|
|
/// line picker) and that cluster's drivers (reusing <see cref="LoadDriversForClusterAsync"/>).
|
|
/// Centralizes the resolution the page would otherwise need the loaded tree to perform. Returns
|
|
/// empty lists when <paramref name="lineId"/> is null/empty or cannot be resolved to a cluster.
|
|
/// </summary>
|
|
/// <param name="lineId">The line whose owning cluster scopes the option lists; null/empty yields empties.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The cluster-scoped line and driver options, or empty lists when the line can't be resolved.</returns>
|
|
Task<EquipmentPickContext> LoadEquipmentPickContextAsync(string? lineId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Creates a new UNS area under a cluster. Fails if an area with the same id already exists.
|
|
/// Whitespace-only notes are stored as <c>null</c>.
|
|
/// </summary>
|
|
/// <param name="clusterId">The owning cluster.</param>
|
|
/// <param name="unsAreaId">The unique area id to create.</param>
|
|
/// <param name="name">The area name.</param>
|
|
/// <param name="notes">Optional notes; whitespace collapses to <c>null</c>.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, or a duplicate-id failure.</returns>
|
|
Task<UnsMutationResult> CreateAreaAsync(string clusterId, string unsAreaId, string name, string? notes, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Updates a UNS area's name, notes, and owning cluster. When the cluster changes, the
|
|
/// reassignment guard blocks the move if any driver-bound equipment under the
|
|
/// area is bound to a driver in a different cluster than the target. Uses last-write-wins
|
|
/// optimistic concurrency on <see cref="Configuration.Entities.UnsArea.RowVersion"/>.
|
|
/// </summary>
|
|
/// <param name="unsAreaId">The area to update.</param>
|
|
/// <param name="name">The new name.</param>
|
|
/// <param name="notes">The new notes; whitespace collapses to <c>null</c>.</param>
|
|
/// <param name="newClusterId">The target cluster (may equal the current one).</param>
|
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a missing-row failure, a cluster-guard failure, or a concurrency failure.</returns>
|
|
Task<UnsMutationResult> UpdateAreaAsync(string unsAreaId, string name, string? notes, string newClusterId, byte[] rowVersion, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Deletes a UNS area. A missing row is treated as success (already gone). Uses last-write-wins
|
|
/// optimistic concurrency; a delete that fails because lines still reference the area surfaces a
|
|
/// guidance message.
|
|
/// </summary>
|
|
/// <param name="unsAreaId">The area to delete.</param>
|
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a concurrency failure, or a delete-failed failure.</returns>
|
|
Task<UnsMutationResult> DeleteAreaAsync(string unsAreaId, byte[] rowVersion, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Creates a new UNS line under an area. Fails if a line with the same id already exists.
|
|
/// Whitespace-only notes are stored as <c>null</c>.
|
|
/// </summary>
|
|
/// <param name="unsAreaId">The owning area.</param>
|
|
/// <param name="unsLineId">The unique line id to create.</param>
|
|
/// <param name="name">The line name.</param>
|
|
/// <param name="notes">Optional notes; whitespace collapses to <c>null</c>.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, or a duplicate-id failure.</returns>
|
|
Task<UnsMutationResult> CreateLineAsync(string unsAreaId, string unsLineId, string name, string? notes, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Updates a UNS line's owning area, name, and notes. Uses last-write-wins optimistic
|
|
/// concurrency on <see cref="Configuration.Entities.UnsLine.RowVersion"/>.
|
|
/// </summary>
|
|
/// <param name="unsLineId">The line to update.</param>
|
|
/// <param name="name">The new name.</param>
|
|
/// <param name="notes">The new notes; whitespace collapses to <c>null</c>.</param>
|
|
/// <param name="newUnsAreaId">The target parent area.</param>
|
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a missing-row failure, or a concurrency failure.</returns>
|
|
Task<UnsMutationResult> UpdateLineAsync(string unsLineId, string name, string? notes, string newUnsAreaId, byte[] rowVersion, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Deletes a UNS line. A missing row is treated as success (already gone). Uses last-write-wins
|
|
/// optimistic concurrency; a delete that fails because equipment still references the line
|
|
/// surfaces a guidance message.
|
|
/// </summary>
|
|
/// <param name="unsLineId">The line to delete.</param>
|
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a concurrency failure, or a delete-failed failure.</returns>
|
|
Task<UnsMutationResult> DeleteLineAsync(string unsLineId, byte[] rowVersion, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Creates a new equipment under a UNS line. The <c>EquipmentId</c> is system-generated
|
|
/// (<c>EQ-</c> + the first 12 hex chars of a fresh <c>EquipmentUuid</c>).
|
|
/// Fails if the line is unset or if the MachineCode is already used fleet-wide. Whitespace-only
|
|
/// ZTag/SAPID collapse to <c>null</c>. (v3: equipment no longer binds a driver — the reference-only
|
|
/// Tags tab points at raw tags instead — so no driver-cluster guard applies.)
|
|
/// </summary>
|
|
/// <param name="input">The operator-editable equipment fields.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a missing-line failure, or a duplicate-MachineCode failure.</returns>
|
|
Task<UnsMutationResult> CreateEquipmentAsync(EquipmentInput input, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Bulk-imports equipment from a parsed set of <see cref="EquipmentInput"/> rows in a single
|
|
/// context, applying the same rules as the single-add path: a row whose <c>UnsLineId</c> does not
|
|
/// exist is an error; and a row whose <c>MachineCode</c> already exists in the DB <em>or</em> earlier
|
|
/// in the same batch is silently skipped (additive-only — never an update). Inserted rows get a
|
|
/// system-generated <c>EQ-</c> id and a fresh <c>EquipmentUuid</c>. All inserts are saved once at
|
|
/// the end. (v3: equipment no longer binds a driver, so there is no per-row driver-cluster guard.)
|
|
/// </summary>
|
|
/// <param name="rows">The parsed equipment rows to import.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>The insert/skip counts and the per-row error list.</returns>
|
|
Task<EquipmentImportResult> ImportEquipmentAsync(IReadOnlyList<EquipmentInput> rows, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Updates an equipment's mutable fields (line, name, MachineCode, external ids, and the OPC 40010
|
|
/// identification fields). Fails on a MachineCode already used by another row. Uses last-write-wins
|
|
/// optimistic concurrency on <see cref="Configuration.Entities.Equipment.RowVersion"/>. (v3: equipment
|
|
/// no longer binds a driver, so there is no driver-cluster guard.)
|
|
/// </summary>
|
|
/// <param name="equipmentId">The equipment to update.</param>
|
|
/// <param name="input">The new operator-editable equipment fields.</param>
|
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a missing-row failure, a duplicate-MachineCode failure, or a concurrency failure.</returns>
|
|
Task<UnsMutationResult> UpdateEquipmentAsync(string equipmentId, EquipmentInput input, byte[] rowVersion, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Deletes an equipment. A missing row is treated as success (already gone). Uses last-write-wins
|
|
/// optimistic concurrency; a delete that fails because tags or virtual tags still reference the
|
|
/// equipment surfaces a guidance message.
|
|
/// </summary>
|
|
/// <param name="equipmentId">The equipment to delete.</param>
|
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a concurrency failure, or a delete-failed failure.</returns>
|
|
Task<UnsMutationResult> DeleteEquipmentAsync(string equipmentId, byte[] rowVersion, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Deletes a server cluster. A missing row is treated as success (already gone). The delete is
|
|
/// refuse-if-children: a cluster that still owns nodes, namespaces, UNS areas, or driver
|
|
/// instances is left intact and a guidance message is returned (the same convention as
|
|
/// Area/Line/Equipment, and matching the existing cluster-settings "Delete cluster" behaviour).
|
|
/// The <see cref="Configuration.Entities.ServerCluster"/> entity carries no concurrency token,
|
|
/// so — unlike the Area/Line/Equipment deletes — this signature takes no <c>rowVersion</c>.
|
|
/// </summary>
|
|
/// <param name="clusterId">The cluster to delete.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, or a refuse-if-children failure naming what still references the cluster.</returns>
|
|
Task<UnsMutationResult> DeleteClusterAsync(string clusterId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Deletes every cluster carrying the given <c>Enterprise</c> label. The enterprise is a tree
|
|
/// label (a string value on cluster rows), not a DB entity, so deleting it means deleting all of
|
|
/// its clusters. The operation is all-or-nothing: it first checks every matching cluster for
|
|
/// children; if <em>any</em> cluster still owns nodes, namespaces, UNS areas, or driver instances
|
|
/// the whole delete refuses (naming the first blocking cluster) and nothing is removed — there is
|
|
/// never a partial enterprise deletion. An enterprise with no matching clusters is a no-op success.
|
|
/// </summary>
|
|
/// <param name="enterpriseName">The <c>Enterprise</c> label whose clusters to delete.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, or a refuse-if-children failure naming the blocking cluster.</returns>
|
|
Task<UnsMutationResult> DeleteEnterpriseAsync(string enterpriseName, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Loads the drivers eligible to back a tag on the given equipment: drivers in the equipment's
|
|
/// cluster (<c>Equipment.UnsLine → UnsArea.ClusterId</c>) whose namespace is Equipment-kind
|
|
/// (tree tags are equipment-bound). Ordered by <c>DriverInstanceId</c>. Returns
|
|
/// an empty list when the equipment cannot be resolved to a cluster.
|
|
/// </summary>
|
|
/// <param name="equipmentId">The equipment whose candidate drivers to load.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The eligible drivers projected to <c>(DriverInstanceId, Display, DriverType, DriverConfig)</c>
|
|
/// tuples, where <c>DriverType</c> lets the TagModal dispatch to a per-driver-type typed config editor
|
|
/// and <c>DriverConfig</c> feeds the live-browse address picker (e.g. the Galaxy gateway picker opens its
|
|
/// browse session against the selected GalaxyMxGateway's config).</returns>
|
|
Task<IReadOnlyList<(string DriverInstanceId, string Display, string DriverType, string DriverConfig)>> LoadTagDriversForEquipmentAsync(string equipmentId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Creates a new equipment-bound tag. <c>FolderPath</c> is always <c>null</c> (the tree
|
|
/// only edits equipment-bound tags). Fails on a duplicate <c>TagId</c>, invalid
|
|
/// <c>TagConfig</c> JSON, an unknown driver, a driver whose namespace is not Equipment-kind, a
|
|
/// driver in a different cluster than the equipment, or a name already used on
|
|
/// the equipment. Whitespace-only <c>PollGroupId</c> collapses to <c>null</c>.
|
|
/// </summary>
|
|
/// <param name="equipmentId">The owning equipment.</param>
|
|
/// <param name="input">The operator-editable tag fields.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, or one of the guard failures.</returns>
|
|
Task<UnsMutationResult> CreateTagAsync(string equipmentId, TagInput input, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Updates an equipment-bound tag's driver binding, name, data type, access level, write-retry
|
|
/// flag, poll group, and config. The owning <c>EquipmentId</c> and the <c>null</c>
|
|
/// <c>FolderPath</c> are preserved. Re-runs the JSON-validity, namespace-kind, and cluster
|
|
/// guards against the tag's existing equipment, and enforces name uniqueness on that
|
|
/// equipment excluding this tag. Uses last-write-wins optimistic concurrency on
|
|
/// <see cref="Configuration.Entities.Tag.RowVersion"/>.
|
|
/// </summary>
|
|
/// <param name="tagId">The tag to update.</param>
|
|
/// <param name="input">The new operator-editable tag fields.</param>
|
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a missing-row failure, a guard failure, or a concurrency failure.</returns>
|
|
Task<UnsMutationResult> UpdateTagAsync(string tagId, TagInput input, byte[] rowVersion, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Deletes a tag. A missing row is treated as success (already gone). Uses last-write-wins
|
|
/// optimistic concurrency on <see cref="Configuration.Entities.Tag.RowVersion"/>.
|
|
/// </summary>
|
|
/// <param name="tagId">The tag to delete.</param>
|
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a concurrency failure, or a delete-failed failure.</returns>
|
|
Task<UnsMutationResult> DeleteTagAsync(string tagId, byte[] rowVersion, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Loads the scripts eligible to back a virtual tag, ordered by name. Each is projected to a
|
|
/// <c>(ScriptId, Display)</c> pair where <c>Display</c> is <c>"{Name} ({Language})"</c>.
|
|
/// </summary>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The scripts projected to <c>(ScriptId, Display)</c> pairs.</returns>
|
|
Task<IReadOnlyList<(string ScriptId, string Display)>> LoadScriptsAsync(CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Creates a new, empty C# script row so the inline virtual-tag panel can bind a brand-new script
|
|
/// without leaving the modal. The <c>ScriptId</c> is system-generated (<c>SC-</c> + the first 12 hex
|
|
/// chars of a fresh <see cref="Guid"/>, mirroring the Script-edit page's convention); the source is
|
|
/// blank, the <c>SourceHash</c> is the SHA-256 of that blank source (so it stays consistent with the
|
|
/// inline save), and the language is <c>CSharp</c>. A whitespace-only name falls back to a default.
|
|
/// The new id is returned via <see cref="UnsMutationResult.CreatedId"/> so the caller can bind it.
|
|
/// </summary>
|
|
/// <param name="name">The operator-friendly script name; whitespace falls back to a default.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success carrying the generated <c>SC-…</c> id in <see cref="UnsMutationResult.CreatedId"/>.</returns>
|
|
Task<UnsMutationResult> CreateScriptAsync(string name, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Creates a new equipment-bound virtual tag (virtual tags are always scoped
|
|
/// to an equipment). Fails if the equipment does not exist, if no script is chosen, if neither a
|
|
/// change trigger nor a timer is set, if the timer is below the 50 ms minimum, on a duplicate
|
|
/// <c>VirtualTagId</c>, or on a name already used on the equipment.
|
|
/// </summary>
|
|
/// <param name="equipmentId">The owning equipment.</param>
|
|
/// <param name="input">The operator-editable virtual-tag fields.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, or one of the guard failures; or if the chosen script uses the <c>{{equip}}</c>
|
|
/// token but the equipment has no derivable single tag base.</returns>
|
|
Task<UnsMutationResult> CreateVirtualTagAsync(string equipmentId, VirtualTagInput input, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Updates an equipment-bound virtual tag's name, data type, script binding, triggers, historize,
|
|
/// and enabled flags. The owning <c>EquipmentId</c> is preserved. Re-runs the script-chosen,
|
|
/// change-or-timer, and 50 ms timer-minimum guards, and enforces name uniqueness on the tag's
|
|
/// existing equipment excluding this virtual tag. Uses last-write-wins optimistic concurrency on
|
|
/// <see cref="Configuration.Entities.VirtualTag.RowVersion"/>.
|
|
/// </summary>
|
|
/// <param name="virtualTagId">The virtual tag to update.</param>
|
|
/// <param name="input">The new operator-editable virtual-tag fields.</param>
|
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a missing-row failure, a guard failure, or a concurrency failure; or if the
|
|
/// chosen script uses the <c>{{equip}}</c> token but the equipment has no derivable single tag base.</returns>
|
|
Task<UnsMutationResult> UpdateVirtualTagAsync(string virtualTagId, VirtualTagInput input, byte[] rowVersion, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Deletes a virtual tag. A missing row is treated as success (already gone). Uses last-write-wins
|
|
/// optimistic concurrency on <see cref="Configuration.Entities.VirtualTag.RowVersion"/>.
|
|
/// </summary>
|
|
/// <param name="virtualTagId">The virtual tag to delete.</param>
|
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a concurrency failure, or a delete-failed failure.</returns>
|
|
Task<UnsMutationResult> DeleteVirtualTagAsync(string virtualTagId, byte[] rowVersion, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Loads a script's editable source for the inline script-source panel in the virtual-tag modal,
|
|
/// along with the concurrency token the panel must echo back on save and the script's display name.
|
|
/// Reads untracked. Returns <c>null</c> when the script no longer exists.
|
|
/// </summary>
|
|
/// <param name="scriptId">The script whose source to load.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The <c>(SourceCode, RowVersion, Name)</c> triple, or <c>null</c> when missing.</returns>
|
|
Task<(string SourceCode, byte[] RowVersion, string Name)?> GetScriptSourceAsync(string scriptId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Counts how many virtual tags bind the given script, so the inline editor can warn the operator
|
|
/// that an edit to a shared script affects every virtual tag using it.
|
|
/// </summary>
|
|
/// <param name="scriptId">The script to count usages of.</param>
|
|
/// <param name="ct">A token to cancel the query.</param>
|
|
/// <returns>The number of virtual tags whose <c>ScriptId</c> matches.</returns>
|
|
Task<int> CountVirtualTagsUsingScriptAsync(string scriptId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Saves an edited script body from the inline panel: updates <c>SourceCode</c> and recomputes the
|
|
/// SHA-256 <c>SourceHash</c> (lower-case hex, matching the Script-edit page). This save is separate
|
|
/// from the virtual-tag save and is guarded by its own last-write-wins optimistic concurrency on
|
|
/// <see cref="Configuration.Entities.Script.RowVersion"/>.
|
|
/// </summary>
|
|
/// <param name="scriptId">The script to update.</param>
|
|
/// <param name="sourceCode">The new source body.</param>
|
|
/// <param name="rowVersion">The concurrency token the panel last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a missing-row failure, or a concurrency failure.</returns>
|
|
Task<(bool Ok, string? Error)> UpdateScriptSourceAsync(string scriptId, string sourceCode, byte[] rowVersion, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Loads the scripted alarms owned by a single equipment as flat row projections for the equipment
|
|
/// page's Alarms tab table, ordered by Name. Each row carries the display columns plus the
|
|
/// <c>ScriptedAlarmId</c> the table uses to open the edit modal. Reads untracked. Returns an empty
|
|
/// list when the equipment has no scripted alarms.
|
|
/// </summary>
|
|
/// <param name="equipmentId">The equipment whose scripted alarms to load.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The equipment's scripted-alarm rows ordered by Name; empty if it has none.</returns>
|
|
Task<IReadOnlyList<EquipmentAlarmRow>> LoadAlarmsForEquipmentAsync(string equipmentId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Loads a single scripted alarm projected for editing, or <c>null</c> if it no longer exists.
|
|
/// Reads untracked and captures the current concurrency token for last-write-wins saves.
|
|
/// </summary>
|
|
/// <param name="scriptedAlarmId">The scripted alarm to load.</param>
|
|
/// <param name="ct">A token to cancel the load.</param>
|
|
/// <returns>The scripted alarm's edit projection, or <c>null</c> when missing.</returns>
|
|
Task<ScriptedAlarmEditDto?> LoadScriptedAlarmAsync(string scriptedAlarmId, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Creates a new scripted alarm under an equipment. The <c>ScriptedAlarmId</c> is operator-typed
|
|
/// (not system-generated). Fails on a duplicate <c>ScriptedAlarmId</c>. Field validation (severity
|
|
/// range, required fields) is enforced client-side in the modal. On success the operator-typed id is
|
|
/// echoed back via <see cref="UnsMutationResult.CreatedId"/>.
|
|
/// </summary>
|
|
/// <param name="equipmentId">The owning equipment.</param>
|
|
/// <param name="input">The operator-editable scripted-alarm fields.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success carrying the created id, or a duplicate-id failure.</returns>
|
|
Task<UnsMutationResult> CreateScriptedAlarmAsync(string equipmentId, ScriptedAlarmInput input, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Updates a scripted alarm's mutable fields (name, alarm type, severity, message template,
|
|
/// predicate script, and the historize/retain/enabled flags). The owning <c>EquipmentId</c> is
|
|
/// preserved (the Alarms tab fixes it). Uses last-write-wins optimistic concurrency on
|
|
/// <see cref="Configuration.Entities.ScriptedAlarm.RowVersion"/>.
|
|
/// </summary>
|
|
/// <param name="scriptedAlarmId">The scripted alarm to update.</param>
|
|
/// <param name="input">The new operator-editable scripted-alarm fields.</param>
|
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a missing-row failure, or a concurrency failure.</returns>
|
|
Task<UnsMutationResult> UpdateScriptedAlarmAsync(string scriptedAlarmId, ScriptedAlarmInput input, byte[] rowVersion, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Deletes a scripted alarm. A missing row is treated as success (already gone). Uses last-write-wins
|
|
/// optimistic concurrency on <see cref="Configuration.Entities.ScriptedAlarm.RowVersion"/>.
|
|
/// </summary>
|
|
/// <param name="scriptedAlarmId">The scripted alarm to delete.</param>
|
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
|
/// <param name="ct">A token to cancel the operation.</param>
|
|
/// <returns>Success, a concurrency failure, or a delete-failed failure.</returns>
|
|
Task<UnsMutationResult> DeleteScriptedAlarmAsync(string scriptedAlarmId, byte[] rowVersion, CancellationToken ct = default);
|
|
}
|