feat(uns): equipment modal wired into the tree

This commit is contained in:
Joseph Doherty
2026-06-08 13:31:14 -04:00
parent 0abd1d8fc2
commit 2beaa43d60
5 changed files with 490 additions and 5 deletions
@@ -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;
}
}