@page "/uns/equipment/new"
@page "/uns/equipment/{EquipmentId}"
@* Dedicated tabbed editor for a single equipment, replacing the /uns EquipmentModal. This task builds
the shell + the Details tab (lifted from EquipmentModal's EditForm) + the create→edit redirect; the
Tags / Virtual Tags / Alarms tabs render placeholders wired in later tasks. On a successful create the
page redirects to /uns/equipment/{newId} so the other tabs (disabled while new) become available. *@
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@rendermode RenderMode.InteractiveServer
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components.Forms
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Uns
@inject IUnsTreeService Svc
@inject NavigationManager Nav
Equipment
@(IsNew ? "New equipment" : (_equipment?.Name ?? EquipmentId))
Back to UNS
@if (_loading)
{
Loading…
}
else if (!IsNew && _equipment is null)
{
}
else
{
_activeTab = "details"'>Details
ShowTabAsync("tags")' disabled="@IsNew">Tags
ShowTabAsync("vtags")' disabled="@IsNew">Virtual Tags
_activeTab = "alarms"' disabled="@IsNew">Alarms
@if (_activeTab == "details")
{
Identity
@if (!IsNew)
{
}
Name
UNS level 5 segment; lowercase letters, digits, dashes, up to 32 chars.
MachineCode
UNS line
— pick a line —
@foreach (var (id, display) in _lineOptions)
{
@display
}
Driver instance
(none / driver-less)
@foreach (var (id, display) in _driverOptions)
{
@display
}
ZTag (ERP)
Unique fleet-wide via ExternalIdReservation.
SAPID
Enabled
Surface in deployments
OPC 40010 identification (optional)
Manufacturer
Model
SerialNumber
HardwareRevision
SoftwareRevision
Year of construction
AssetLocation
ManufacturerUri
DeviceManualUri
@if (!string.IsNullOrWhiteSpace(_error)) { @_error
}
@if (_busy) { }
@(IsNew ? "Create" : "Save changes")
}
else if (_activeTab == "tags")
{
Add tag
@if (!string.IsNullOrWhiteSpace(_tagError))
{
@_tagError
}
@if (_tags is null)
{
Loading…
}
else if (_tags.Count == 0)
{
No tags yet.
}
else
{
Name Driver Data type Access Actions
@foreach (var t in _tags)
{
@t.Name
@t.DriverInstanceId
@t.DataType
@t.AccessLevel
OpenEditTag(t.TagId)">Edit
DeleteTag(t.TagId)">Delete
}
}
}
else if (_activeTab == "vtags")
{
Add virtual tag
@if (!string.IsNullOrWhiteSpace(_vtagError))
{
@_vtagError
}
@if (_vtags is null)
{
Loading…
}
else if (_vtags.Count == 0)
{
No virtual tags yet.
}
else
{
Name Data type Script Enabled Actions
@foreach (var v in _vtags)
{
@v.Name
@v.DataType
@v.ScriptId
@(v.Enabled ? "Yes" : "No")
OpenEditVirtualTag(v.VirtualTagId)">Edit
DeleteVirtualTag(v.VirtualTagId)">Delete
}
}
}
else if (_activeTab == "alarms") { Alarms tab — wired in a later task.
}
}
@code {
/// The equipment id from the route; null/empty on the /new route (create mode).
[Parameter] public string? EquipmentId { get; set; }
/// Optional parent line id supplied as a query string on create, used to default the line select.
[SupplyParameterFromQuery] public string? LineId { get; set; }
private bool IsNew => string.IsNullOrEmpty(EquipmentId);
private string _activeTab = "details";
private bool _loading = true;
private bool _busy;
private string? _error;
private EquipmentEditDto? _equipment;
private FormModel _form = new();
private IReadOnlyList<(string Id, string Display)> _lineOptions = Array.Empty<(string, string)>();
private IReadOnlyList<(string Id, string Display)> _driverOptions = Array.Empty<(string, string)>();
// --- Tags tab state. _tags is null until the tab is first activated (drives the lazy load + spinner). ---
private IReadOnlyList? _tags;
private string? _tagError;
private bool _tagModalVisible;
private bool _tagModalIsNew;
private TagEditDto? _tagModalExisting;
private IReadOnlyList<(string Id, string Display, string DriverType)> _tagDriverOptions = Array.Empty<(string, string, string)>();
// --- Virtual Tags tab state. _vtags is null until the tab is first activated. ---
private IReadOnlyList? _vtags;
private string? _vtagError;
private bool _vtagModalVisible;
private bool _vtagModalIsNew;
private VirtualTagEditDto? _vtagModalExisting;
private IReadOnlyList<(string Id, string Display)> _vtagScriptOptions = Array.Empty<(string, string)>();
private string TabClass(string tab) => _activeTab == tab ? "active" : "";
///
/// Switches to a tab, lazily loading its list on first activation. The Tags/Virtual Tags lists are
/// null until first shown (and are reset to null in OnParametersSetAsync when the equipment changes),
/// so the fetch runs once per equipment rather than on every render. Equipment is fixed to
/// ; the tabs are disabled while IsNew so this is only ever reached with a
/// persisted equipment.
///
private async Task ShowTabAsync(string tab)
{
_activeTab = tab;
if (IsNew) { return; }
if (tab == "tags" && _tags is null) { await ReloadTagsAsync(); }
else if (tab == "vtags" && _vtags is null) { await ReloadVirtualTagsAsync(); }
}
// --- Tags tab handlers (mirror GlobalUns; the owning equipment is fixed = EquipmentId) ---
private async Task ReloadTagsAsync()
{
_tags = await Svc.LoadTagsForEquipmentAsync(EquipmentId!);
}
private async Task OpenAddTag()
{
_tagError = null;
_tagModalIsNew = true;
_tagModalExisting = null;
_tagDriverOptions = await Svc.LoadTagDriversForEquipmentAsync(EquipmentId!);
_tagModalVisible = true;
}
private async Task OpenEditTag(string tagId)
{
_tagError = null;
var dto = await Svc.LoadTagAsync(tagId);
if (dto is null) { _tagError = "That tag no longer exists; the list was refreshed."; await ReloadTagsAsync(); return; }
_tagModalIsNew = false;
_tagModalExisting = dto;
_tagDriverOptions = await Svc.LoadTagDriversForEquipmentAsync(EquipmentId!);
_tagModalVisible = true;
}
private async Task OnTagSavedAsync()
{
_tagModalVisible = false;
await ReloadTagsAsync();
}
private async Task DeleteTag(string tagId)
{
_tagModalVisible = false;
_tagError = null;
// Load the tag fresh to capture its current RowVersion for the optimistic-concurrency delete.
var dto = await Svc.LoadTagAsync(tagId);
if (dto is null) { await ReloadTagsAsync(); return; }
var r = await Svc.DeleteTagAsync(tagId, dto.RowVersion);
if (r.Ok) { await ReloadTagsAsync(); }
else { _tagError = r.Error; }
}
// --- Virtual Tags tab handlers ---
private async Task ReloadVirtualTagsAsync()
{
_vtags = await Svc.LoadVirtualTagsForEquipmentAsync(EquipmentId!);
}
private async Task OpenAddVirtualTag()
{
_vtagError = null;
_vtagModalIsNew = true;
_vtagModalExisting = null;
_vtagScriptOptions = await Svc.LoadScriptsAsync();
_vtagModalVisible = true;
}
private async Task OpenEditVirtualTag(string vtagId)
{
_vtagError = null;
var dto = await Svc.LoadVirtualTagAsync(vtagId);
if (dto is null) { _vtagError = "That virtual tag no longer exists; the list was refreshed."; await ReloadVirtualTagsAsync(); return; }
_vtagModalIsNew = false;
_vtagModalExisting = dto;
_vtagScriptOptions = await Svc.LoadScriptsAsync();
_vtagModalVisible = true;
}
private async Task OnVirtualTagSavedAsync()
{
_vtagModalVisible = false;
await ReloadVirtualTagsAsync();
}
private async Task DeleteVirtualTag(string vtagId)
{
_vtagModalVisible = false;
_vtagError = null;
// Load the virtual tag fresh to capture its current RowVersion for the concurrency-guarded delete.
var dto = await Svc.LoadVirtualTagAsync(vtagId);
if (dto is null) { await ReloadVirtualTagsAsync(); return; }
var r = await Svc.DeleteVirtualTagAsync(vtagId, dto.RowVersion);
if (r.Ok) { await ReloadVirtualTagsAsync(); }
else { _vtagError = r.Error; }
}
protected override async Task OnParametersSetAsync()
{
_loading = true;
_error = null;
// _activeTab is intentionally NOT reset here: an in-place save reloads the page (re-runs
// OnParametersSetAsync) and the user's tab selection should survive that. The create→redirect
// path lands on Details because the field initializes to "details" and a fresh page instance
// starts with that initial value. The Tags/Virtual Tags lists are reset to null below so the
// lazy loaders re-fetch for the (possibly different) equipment this parameter set targets.
_tags = null;
_vtags = null;
if (!IsNew)
{
_equipment = await Svc.LoadEquipmentAsync(EquipmentId!);
if (_equipment is not null)
{
LoadFormFrom(_equipment);
var ctx = await Svc.LoadEquipmentPickContextAsync(_equipment.UnsLineId);
_lineOptions = ctx.Lines;
_driverOptions = ctx.Drivers;
}
}
else
{
_form = new FormModel { UnsLineId = LineId ?? "" };
var ctx = await Svc.LoadEquipmentPickContextAsync(LineId);
_lineOptions = ctx.Lines;
_driverOptions = ctx.Drivers;
}
_loading = false;
}
/// Maps a loaded equipment's fields into the working form (mirrors EquipmentModal's edit branch).
private void LoadFormFrom(EquipmentEditDto e)
{
_form = new FormModel
{
Name = e.Name,
MachineCode = e.MachineCode,
UnsLineId = e.UnsLineId,
DriverInstanceId = e.DriverInstanceId,
ZTag = e.ZTag,
SAPID = e.SAPID,
Manufacturer = e.Manufacturer,
Model = e.Model,
SerialNumber = e.SerialNumber,
HardwareRevision = e.HardwareRevision,
SoftwareRevision = e.SoftwareRevision,
YearOfConstruction = e.YearOfConstruction,
AssetLocation = e.AssetLocation,
ManufacturerUri = e.ManufacturerUri,
DeviceManualUri = e.DeviceManualUri,
Enabled = e.Enabled,
};
}
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(_equipment!.EquipmentId, input, _equipment.RowVersion);
if (!result.Ok) { _error = result.Error; return; }
if (IsNew)
{
// Redirect to the persisted editor so the other tabs (disabled while new) become available.
Nav.NavigateTo($"/uns/equipment/{result.CreatedId}");
}
else
{
// Reload to pick up the fresh RowVersion for the next save.
_equipment = await Svc.LoadEquipmentAsync(EquipmentId!);
if (_equipment is not null) { LoadFormFrom(_equipment); }
}
}
finally
{
_busy = false;
}
}
/// The working form for the Details tab — identical to EquipmentModal.FormModel.
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;
}
}