feat(uns): equipment modal wired into the tree
This commit is contained in:
@@ -63,6 +63,15 @@
|
|||||||
OnSaved="OnModalSavedAsync"
|
OnSaved="OnModalSavedAsync"
|
||||||
OnCancel="CloseModals" />
|
OnCancel="CloseModals" />
|
||||||
|
|
||||||
|
<EquipmentModal Visible="_equipmentModalVisible"
|
||||||
|
IsNew="_equipmentModalIsNew"
|
||||||
|
UnsLineId="_equipmentModalLineId"
|
||||||
|
Existing="_equipmentModalExisting"
|
||||||
|
Lines="_equipmentModalLineOptions"
|
||||||
|
Drivers="_equipmentModalDriverOptions"
|
||||||
|
OnSaved="OnModalSavedAsync"
|
||||||
|
OnCancel="CloseModals" />
|
||||||
|
|
||||||
@if (_confirmNode is not null)
|
@if (_confirmNode is not null)
|
||||||
{
|
{
|
||||||
<div class="modal-backdrop fade show" style="display:block"></div>
|
<div class="modal-backdrop fade show" style="display:block"></div>
|
||||||
@@ -110,6 +119,14 @@
|
|||||||
private LineEditDto? _lineModalExisting;
|
private LineEditDto? _lineModalExisting;
|
||||||
private IReadOnlyList<(string Id, string Display)> _lineModalAreaOptions = Array.Empty<(string, string)>();
|
private IReadOnlyList<(string Id, string Display)> _lineModalAreaOptions = Array.Empty<(string, string)>();
|
||||||
|
|
||||||
|
// --- Equipment modal state ---
|
||||||
|
private bool _equipmentModalVisible;
|
||||||
|
private bool _equipmentModalIsNew;
|
||||||
|
private string? _equipmentModalLineId;
|
||||||
|
private EquipmentEditDto? _equipmentModalExisting;
|
||||||
|
private IReadOnlyList<(string Id, string Display)> _equipmentModalLineOptions = Array.Empty<(string, string)>();
|
||||||
|
private IReadOnlyList<(string Id, string Display)> _equipmentModalDriverOptions = Array.Empty<(string, string)>();
|
||||||
|
|
||||||
// --- Delete-confirm state ---
|
// --- Delete-confirm state ---
|
||||||
private UnsNode? _confirmNode;
|
private UnsNode? _confirmNode;
|
||||||
private bool _confirmBusy;
|
private bool _confirmBusy;
|
||||||
@@ -139,6 +156,18 @@
|
|||||||
.Select(a => (a.EntityId!, a.DisplayName))
|
.Select(a => (a.EntityId!, a.DisplayName))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
/// <summary>Returns the <c>(Id, Display)</c> line options inside a single cluster, for the equipment picker.</summary>
|
||||||
|
private IReadOnlyList<(string Id, string Display)> LinesForCluster(string? clusterId) =>
|
||||||
|
_roots
|
||||||
|
.SelectMany(ent => ent.Children)
|
||||||
|
.Where(c => c.Kind == UnsNodeKind.Cluster && c.ClusterId == clusterId)
|
||||||
|
.SelectMany(c => c.Children)
|
||||||
|
.Where(a => a.Kind == UnsNodeKind.Area)
|
||||||
|
.SelectMany(a => a.Children)
|
||||||
|
.Where(l => l.Kind == UnsNodeKind.Line && l.EntityId is not null)
|
||||||
|
.Select(l => (l.EntityId!, l.DisplayName))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Toggles a node's expansion. For equipment nodes whose children have not yet
|
/// Toggles a node's expansion. For equipment nodes whose children have not yet
|
||||||
/// been loaded, lazily fetches the tag/virtual-tag leaves on first expand.
|
/// been loaded, lazily fetches the tag/virtual-tag leaves on first expand.
|
||||||
@@ -175,9 +204,10 @@
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Opens the create modal for a node's primary child: a cluster gets a new area; an area gets a
|
/// Opens the create modal for a node's primary child: a cluster gets a new area; an area gets a
|
||||||
/// new line scoped to its cluster. Equipment "+ Tag" is handled in a later task.
|
/// new line scoped to its cluster; a line gets a new equipment scoped to its cluster. Equipment
|
||||||
|
/// "+ Tag" is handled in a later task.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void HandleAddChild(UnsNode node)
|
private async Task HandleAddChild(UnsNode node)
|
||||||
{
|
{
|
||||||
CloseModals();
|
CloseModals();
|
||||||
switch (node.Kind)
|
switch (node.Kind)
|
||||||
@@ -196,6 +226,15 @@
|
|||||||
_lineModalAreaOptions = AreaOptionsForCluster(node.ClusterId);
|
_lineModalAreaOptions = AreaOptionsForCluster(node.ClusterId);
|
||||||
_lineModalVisible = true;
|
_lineModalVisible = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case UnsNodeKind.Line:
|
||||||
|
_equipmentModalIsNew = true;
|
||||||
|
_equipmentModalExisting = null;
|
||||||
|
_equipmentModalLineId = node.EntityId;
|
||||||
|
_equipmentModalLineOptions = LinesForCluster(node.ClusterId);
|
||||||
|
_equipmentModalDriverOptions = await Svc.LoadDriversForClusterAsync(node.ClusterId!);
|
||||||
|
_equipmentModalVisible = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,6 +265,17 @@
|
|||||||
_lineModalAreaOptions = AreaOptionsForCluster(node.ClusterId);
|
_lineModalAreaOptions = AreaOptionsForCluster(node.ClusterId);
|
||||||
_lineModalVisible = true;
|
_lineModalVisible = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case UnsNodeKind.Equipment:
|
||||||
|
var equipment = await Svc.LoadEquipmentAsync(node.EntityId!);
|
||||||
|
if (equipment is null) { return; }
|
||||||
|
_equipmentModalIsNew = false;
|
||||||
|
_equipmentModalExisting = equipment;
|
||||||
|
_equipmentModalLineId = equipment.UnsLineId;
|
||||||
|
_equipmentModalLineOptions = LinesForCluster(node.ClusterId);
|
||||||
|
_equipmentModalDriverOptions = await Svc.LoadDriversForClusterAsync(node.ClusterId!);
|
||||||
|
_equipmentModalVisible = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +288,7 @@
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs the pending delete. Loads the entity's RowVersion first, then dispatches on Kind.
|
/// Performs the pending delete. Loads the entity's RowVersion first, then dispatches on Kind.
|
||||||
/// Area/Line are handled here; other kinds are wired in later tasks.
|
/// Area/Line/Equipment are handled here; Tag/VirtualTag are wired in later tasks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task ConfirmDeleteAsync()
|
private async Task ConfirmDeleteAsync()
|
||||||
{
|
{
|
||||||
@@ -264,8 +314,14 @@
|
|||||||
result = await Svc.DeleteLineAsync(node.EntityId!, line.RowVersion);
|
result = await Svc.DeleteLineAsync(node.EntityId!, line.RowVersion);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case UnsNodeKind.Equipment:
|
||||||
|
var equipment = await Svc.LoadEquipmentAsync(node.EntityId!);
|
||||||
|
if (equipment is null) { await ReloadAndCloseAsync(); return; }
|
||||||
|
result = await Svc.DeleteEquipmentAsync(node.EntityId!, equipment.RowVersion);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Equipment/Tag/VirtualTag deletes are wired in later tasks.
|
// Tag/VirtualTag deletes are wired in later tasks.
|
||||||
result = new UnsMutationResult(false, "Delete for this node kind is not yet available.");
|
result = new UnsMutationResult(false, "Delete for this node kind is not yet available.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -309,6 +365,10 @@
|
|||||||
_lineModalVisible = false;
|
_lineModalVisible = false;
|
||||||
_lineModalExisting = null;
|
_lineModalExisting = null;
|
||||||
_lineModalAreaOptions = Array.Empty<(string, string)>();
|
_lineModalAreaOptions = Array.Empty<(string, string)>();
|
||||||
|
_equipmentModalVisible = false;
|
||||||
|
_equipmentModalExisting = null;
|
||||||
|
_equipmentModalLineOptions = Array.Empty<(string, string)>();
|
||||||
|
_equipmentModalDriverOptions = Array.Empty<(string, string)>();
|
||||||
_confirmNode = null;
|
_confirmNode = null;
|
||||||
_confirmError = null;
|
_confirmError = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
@* Create/edit modal for an equipment, wired straight into IUnsTreeService. The host page owns
|
||||||
|
visibility and supplies the parent line id (create) or the loaded EquipmentEditDto (edit), plus
|
||||||
|
the cluster-scoped UNS-line and driver lists. The EquipmentId is system-generated (decision #125)
|
||||||
|
so it is never an editable field — only shown read-only on edit. On a successful save it raises
|
||||||
|
OnSaved so the host can reload the tree. *@
|
||||||
|
@using System.ComponentModel.DataAnnotations
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||||
|
@inject IUnsTreeService Svc
|
||||||
|
|
||||||
|
@if (Visible)
|
||||||
|
{
|
||||||
|
<div class="modal-backdrop fade show" style="display:block"></div>
|
||||||
|
<div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
|
||||||
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<EditForm Model="_form" OnValidSubmit="SaveAsync" FormName="equipmentModal">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">@(IsNew ? "New equipment" : "Edit equipment")</h5>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" @onclick="CancelAsync"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<h6 class="text-muted">Identity</h6>
|
||||||
|
@if (!IsNew)
|
||||||
|
{
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">EquipmentId</label>
|
||||||
|
<input class="form-control form-control-sm mono" value="@Existing?.EquipmentId" disabled />
|
||||||
|
<div class="form-text">System-generated; never operator-edited.</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label" for="eq-name">Name</label>
|
||||||
|
<InputText id="eq-name" @bind-Value="_form.Name" class="form-control form-control-sm mono"
|
||||||
|
placeholder="machine-01" />
|
||||||
|
<div class="form-text">UNS level 5 segment; lowercase letters, digits, dashes, up to 32 chars.</div>
|
||||||
|
<ValidationMessage For="@(() => _form.Name)" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label" for="eq-machinecode">MachineCode</label>
|
||||||
|
<InputText id="eq-machinecode" @bind-Value="_form.MachineCode" class="form-control form-control-sm mono"
|
||||||
|
placeholder="machine_001" />
|
||||||
|
<ValidationMessage For="@(() => _form.MachineCode)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label" for="eq-line">UNS line</label>
|
||||||
|
<InputSelect id="eq-line" @bind-Value="_form.UnsLineId" class="form-select form-select-sm">
|
||||||
|
<option value="">— pick a line —</option>
|
||||||
|
@foreach (var (id, display) in Lines)
|
||||||
|
{
|
||||||
|
<option value="@id">@display</option>
|
||||||
|
}
|
||||||
|
</InputSelect>
|
||||||
|
<ValidationMessage For="@(() => _form.UnsLineId)" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label" for="eq-driver">Driver instance</label>
|
||||||
|
<InputSelect id="eq-driver" @bind-Value="_form.DriverInstanceId" class="form-select form-select-sm">
|
||||||
|
<option value="">(none / driver-less)</option>
|
||||||
|
@foreach (var (id, display) in Drivers)
|
||||||
|
{
|
||||||
|
<option value="@id">@display</option>
|
||||||
|
}
|
||||||
|
</InputSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label" for="eq-ztag">ZTag (ERP)</label>
|
||||||
|
<InputText id="eq-ztag" @bind-Value="_form.ZTag" class="form-control form-control-sm" />
|
||||||
|
<div class="form-text">Unique fleet-wide via ExternalIdReservation.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label" for="eq-sap">SAPID</label>
|
||||||
|
<InputText id="eq-sap" @bind-Value="_form.SAPID" class="form-control form-control-sm" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Enabled</label>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<InputCheckbox @bind-Value="_form.Enabled" class="form-check-input" />
|
||||||
|
<label class="form-check-label">Surface in deployments</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<h6 class="text-muted">OPC 40010 identification (optional)</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3"><label class="form-label">Manufacturer</label><InputText @bind-Value="_form.Manufacturer" class="form-control form-control-sm" /></div>
|
||||||
|
<div class="col-md-4 mb-3"><label class="form-label">Model</label><InputText @bind-Value="_form.Model" class="form-control form-control-sm" /></div>
|
||||||
|
<div class="col-md-4 mb-3"><label class="form-label">SerialNumber</label><InputText @bind-Value="_form.SerialNumber" class="form-control form-control-sm" /></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 mb-3"><label class="form-label">HardwareRevision</label><InputText @bind-Value="_form.HardwareRevision" class="form-control form-control-sm" /></div>
|
||||||
|
<div class="col-md-3 mb-3"><label class="form-label">SoftwareRevision</label><InputText @bind-Value="_form.SoftwareRevision" class="form-control form-control-sm" /></div>
|
||||||
|
<div class="col-md-3 mb-3"><label class="form-label">Year of construction</label><InputNumber @bind-Value="_form.YearOfConstruction" class="form-control form-control-sm" /></div>
|
||||||
|
<div class="col-md-3 mb-3"><label class="form-label">AssetLocation</label><InputText @bind-Value="_form.AssetLocation" class="form-control form-control-sm" /></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3"><label class="form-label">ManufacturerUri</label><InputText @bind-Value="_form.ManufacturerUri" class="form-control form-control-sm mono" /></div>
|
||||||
|
<div class="col-md-6 mb-3"><label class="form-label">DeviceManualUri</label><InputText @bind-Value="_form.DeviceManualUri" class="form-control form-control-sm mono" /></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_error))
|
||||||
|
{
|
||||||
|
<div class="text-danger small mt-2">@_error</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" @onclick="CancelAsync" disabled="@_busy">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" disabled="@_busy">
|
||||||
|
@if (_busy) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||||
|
@(IsNew ? "Create" : "Save changes")
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</EditForm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>Whether the modal is shown. The host owns this flag.</summary>
|
||||||
|
[Parameter] public bool Visible { get; set; }
|
||||||
|
|
||||||
|
/// <summary><c>true</c> to create a new equipment; <c>false</c> to edit <see cref="Existing"/>.</summary>
|
||||||
|
[Parameter] public bool IsNew { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The parent line id used to default the UNS-line select on create.</summary>
|
||||||
|
[Parameter] public string? UnsLineId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The equipment being edited, when <see cref="IsNew"/> is <c>false</c>.</summary>
|
||||||
|
[Parameter] public EquipmentEditDto? Existing { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The selectable UNS lines — scoped to the equipment's cluster by the host — as <c>(Id, Display)</c> pairs.</summary>
|
||||||
|
[Parameter] public IReadOnlyList<(string Id, string Display)> Lines { get; set; } = Array.Empty<(string, string)>();
|
||||||
|
|
||||||
|
/// <summary>The selectable drivers — scoped to the equipment's cluster by the host — as <c>(Id, Display)</c> pairs.</summary>
|
||||||
|
[Parameter] public IReadOnlyList<(string Id, string Display)> Drivers { get; set; } = Array.Empty<(string, string)>();
|
||||||
|
|
||||||
|
/// <summary>Raised after a successful create/save so the host can reload and close.</summary>
|
||||||
|
[Parameter] public EventCallback OnSaved { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Raised when the user cancels so the host can close.</summary>
|
||||||
|
[Parameter] public EventCallback OnCancel { get; set; }
|
||||||
|
|
||||||
|
private FormModel _form = new();
|
||||||
|
private bool _busy;
|
||||||
|
private string? _error;
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
// Rebuild the working form whenever the host (re)opens the modal for a fresh target.
|
||||||
|
if (IsNew)
|
||||||
|
{
|
||||||
|
_form = new FormModel { UnsLineId = UnsLineId ?? "" };
|
||||||
|
}
|
||||||
|
else if (Existing is not null)
|
||||||
|
{
|
||||||
|
_form = new FormModel
|
||||||
|
{
|
||||||
|
Name = Existing.Name,
|
||||||
|
MachineCode = Existing.MachineCode,
|
||||||
|
UnsLineId = Existing.UnsLineId,
|
||||||
|
DriverInstanceId = Existing.DriverInstanceId,
|
||||||
|
ZTag = Existing.ZTag,
|
||||||
|
SAPID = Existing.SAPID,
|
||||||
|
Manufacturer = Existing.Manufacturer,
|
||||||
|
Model = Existing.Model,
|
||||||
|
SerialNumber = Existing.SerialNumber,
|
||||||
|
HardwareRevision = Existing.HardwareRevision,
|
||||||
|
SoftwareRevision = Existing.SoftwareRevision,
|
||||||
|
YearOfConstruction = Existing.YearOfConstruction,
|
||||||
|
AssetLocation = Existing.AssetLocation,
|
||||||
|
ManufacturerUri = Existing.ManufacturerUri,
|
||||||
|
DeviceManualUri = Existing.DeviceManualUri,
|
||||||
|
Enabled = Existing.Enabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveAsync()
|
||||||
|
{
|
||||||
|
_busy = true;
|
||||||
|
_error = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var input = new EquipmentInput(
|
||||||
|
_form.Name,
|
||||||
|
_form.MachineCode,
|
||||||
|
_form.UnsLineId,
|
||||||
|
_form.DriverInstanceId,
|
||||||
|
_form.ZTag,
|
||||||
|
_form.SAPID,
|
||||||
|
_form.Manufacturer,
|
||||||
|
_form.Model,
|
||||||
|
_form.SerialNumber,
|
||||||
|
_form.HardwareRevision,
|
||||||
|
_form.SoftwareRevision,
|
||||||
|
_form.YearOfConstruction,
|
||||||
|
_form.AssetLocation,
|
||||||
|
_form.ManufacturerUri,
|
||||||
|
_form.DeviceManualUri,
|
||||||
|
_form.Enabled);
|
||||||
|
|
||||||
|
var result = IsNew
|
||||||
|
? await Svc.CreateEquipmentAsync(input)
|
||||||
|
: await Svc.UpdateEquipmentAsync(Existing!.EquipmentId, input, Existing.RowVersion);
|
||||||
|
|
||||||
|
if (result.Ok)
|
||||||
|
{
|
||||||
|
await OnSaved.InvokeAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_error = result.Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task CancelAsync() => OnCancel.InvokeAsync();
|
||||||
|
|
||||||
|
private sealed class FormModel
|
||||||
|
{
|
||||||
|
[Required, RegularExpression("^[a-z0-9-]{1,32}$", ErrorMessage = "Lowercase letters, digits, dashes only; max 32 chars.")]
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
[Required] public string MachineCode { get; set; } = "";
|
||||||
|
[Required] public string UnsLineId { get; set; } = "";
|
||||||
|
public string? DriverInstanceId { get; set; }
|
||||||
|
public string? ZTag { get; set; }
|
||||||
|
public string? SAPID { get; set; }
|
||||||
|
public string? Manufacturer { get; set; }
|
||||||
|
public string? Model { get; set; }
|
||||||
|
public string? SerialNumber { get; set; }
|
||||||
|
public string? HardwareRevision { get; set; }
|
||||||
|
public string? SoftwareRevision { get; set; }
|
||||||
|
public short? YearOfConstruction { get; set; }
|
||||||
|
public string? AssetLocation { get; set; }
|
||||||
|
public string? ManufacturerUri { get; set; }
|
||||||
|
public string? DeviceManualUri { get; set; }
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,33 @@ public sealed record AreaEditDto(string UnsAreaId, string Name, string? Notes, s
|
|||||||
/// <param name="RowVersion">The optimistic-concurrency token last read.</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);
|
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, decision #125).</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>
|
/// <summary>
|
||||||
/// Loads the structural portion of the unified-namespace (UNS) browse tree —
|
/// Loads the structural portion of the unified-namespace (UNS) browse tree —
|
||||||
/// Enterprise → Cluster → Area → Line → Equipment — from the config database.
|
/// Enterprise → Cluster → Area → Line → Equipment — from the config database.
|
||||||
@@ -67,6 +94,26 @@ public interface IUnsTreeService
|
|||||||
/// <returns>The line's edit projection, or <c>null</c> when missing.</returns>
|
/// <returns>The line's edit projection, or <c>null</c> when missing.</returns>
|
||||||
Task<LineEditDto?> LoadLineAsync(string unsLineId, CancellationToken ct = default);
|
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 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>
|
/// <summary>
|
||||||
/// Creates a new UNS area under a cluster. Fails if an area with the same id already exists.
|
/// 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>.
|
/// Whitespace-only notes are stored as <c>null</c>.
|
||||||
|
|||||||
@@ -148,6 +148,55 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
|||||||
.FirstOrDefaultAsync(ct);
|
.FirstOrDefaultAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<EquipmentEditDto?> LoadEquipmentAsync(string equipmentId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||||
|
|
||||||
|
return await db.Equipment
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(e => e.EquipmentId == equipmentId)
|
||||||
|
.Select(e => new EquipmentEditDto(
|
||||||
|
e.EquipmentId,
|
||||||
|
e.Name,
|
||||||
|
e.MachineCode,
|
||||||
|
e.UnsLineId,
|
||||||
|
e.DriverInstanceId,
|
||||||
|
e.ZTag,
|
||||||
|
e.SAPID,
|
||||||
|
e.Manufacturer,
|
||||||
|
e.Model,
|
||||||
|
e.SerialNumber,
|
||||||
|
e.HardwareRevision,
|
||||||
|
e.SoftwareRevision,
|
||||||
|
e.YearOfConstruction,
|
||||||
|
e.AssetLocation,
|
||||||
|
e.ManufacturerUri,
|
||||||
|
e.DeviceManualUri,
|
||||||
|
e.Enabled,
|
||||||
|
e.RowVersion))
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<(string DriverInstanceId, string Display)>> LoadDriversForClusterAsync(
|
||||||
|
string clusterId,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||||
|
|
||||||
|
var drivers = await db.DriverInstances
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(d => d.ClusterId == clusterId)
|
||||||
|
.OrderBy(d => d.DriverInstanceId)
|
||||||
|
.Select(d => new { d.DriverInstanceId, d.Name, d.DriverType })
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
return drivers
|
||||||
|
.Select(d => (d.DriverInstanceId, Display: $"{d.DriverInstanceId} — {d.Name} ({d.DriverType})"))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<UnsMutationResult> CreateAreaAsync(
|
public async Task<UnsMutationResult> CreateAreaAsync(
|
||||||
string clusterId,
|
string clusterId,
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Verifies the load-for-edit projections on <see cref="UnsTreeService"/> that prefill the
|
/// Verifies the load-for-edit projections on <see cref="UnsTreeService"/> that prefill the
|
||||||
/// Area/Line edit modals and carry the concurrency token back for last-write-wins saves.
|
/// Area/Line/Equipment edit modals and carry the concurrency token back for last-write-wins saves.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Trait("Category", "Unit")]
|
[Trait("Category", "Unit")]
|
||||||
public sealed class UnsTreeServiceLoadEditTests
|
public sealed class UnsTreeServiceLoadEditTests
|
||||||
@@ -65,4 +66,80 @@ public sealed class UnsTreeServiceLoadEditTests
|
|||||||
|
|
||||||
(await service.LoadLineAsync("NOPE")).ShouldBeNull();
|
(await service.LoadLineAsync("NOPE")).ShouldBeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Loading the seeded equipment maps its identity fields, line, and a non-empty RowVersion.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task LoadEquipment_returns_dto()
|
||||||
|
{
|
||||||
|
var (service, _) = Seeded();
|
||||||
|
|
||||||
|
var dto = await service.LoadEquipmentAsync(UnsTreeTestDb.SeededEquipmentId);
|
||||||
|
|
||||||
|
dto.ShouldNotBeNull();
|
||||||
|
dto.EquipmentId.ShouldBe(UnsTreeTestDb.SeededEquipmentId);
|
||||||
|
dto.Name.ShouldBe("machine-1");
|
||||||
|
dto.MachineCode.ShouldBe("machine_001");
|
||||||
|
dto.UnsLineId.ShouldBe("LINE-1");
|
||||||
|
dto.RowVersion.ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Loading a missing equipment returns null.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task LoadEquipment_missing_returns_null()
|
||||||
|
{
|
||||||
|
var (service, _) = Seeded();
|
||||||
|
|
||||||
|
(await service.LoadEquipmentAsync("NOPE")).ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// LoadDriversForCluster returns every driver in the cluster (any namespace kind), ordered by id,
|
||||||
|
/// with the <c>"{Id} — {Name} ({DriverType})"</c> display; drivers in other clusters are excluded.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task LoadDriversForCluster_returns_cluster_drivers()
|
||||||
|
{
|
||||||
|
var (service, dbName) = Seeded();
|
||||||
|
|
||||||
|
await using (var db = UnsTreeTestDb.CreateNamed(dbName))
|
||||||
|
{
|
||||||
|
db.DriverInstances.Add(new DriverInstance
|
||||||
|
{
|
||||||
|
DriverInstanceId = "DRV-B",
|
||||||
|
ClusterId = UnsTreeTestDb.PopulatedClusterId,
|
||||||
|
NamespaceId = "NS-1",
|
||||||
|
Name = "modbus-b",
|
||||||
|
DriverType = "ModbusTcp",
|
||||||
|
DriverConfig = "{}",
|
||||||
|
});
|
||||||
|
db.DriverInstances.Add(new DriverInstance
|
||||||
|
{
|
||||||
|
DriverInstanceId = "DRV-A",
|
||||||
|
ClusterId = UnsTreeTestDb.PopulatedClusterId,
|
||||||
|
NamespaceId = "NS-1",
|
||||||
|
Name = "galaxy-a",
|
||||||
|
DriverType = "Galaxy",
|
||||||
|
DriverConfig = "{}",
|
||||||
|
});
|
||||||
|
// A driver in a different cluster must be excluded.
|
||||||
|
db.DriverInstances.Add(new DriverInstance
|
||||||
|
{
|
||||||
|
DriverInstanceId = "DRV-OTHER",
|
||||||
|
ClusterId = UnsTreeTestDb.EmptyClusterId,
|
||||||
|
NamespaceId = "NS-2",
|
||||||
|
Name = "other",
|
||||||
|
DriverType = "S7",
|
||||||
|
DriverConfig = "{}",
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var drivers = await service.LoadDriversForClusterAsync(UnsTreeTestDb.PopulatedClusterId);
|
||||||
|
|
||||||
|
drivers.Count.ShouldBe(2);
|
||||||
|
drivers[0].DriverInstanceId.ShouldBe("DRV-A");
|
||||||
|
drivers[0].Display.ShouldBe("DRV-A — galaxy-a (Galaxy)");
|
||||||
|
drivers[1].DriverInstanceId.ShouldBe("DRV-B");
|
||||||
|
drivers[1].Display.ShouldBe("DRV-B — modbus-b (ModbusTcp)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user