@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 [Authorize(Policy = AdminUiPolicies.ConfigEditor)] @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) {
@EquipmentId not found.
} else { @if (_activeTab == "details") {
Identity
@if (!IsNew) {
System-generated; never operator-edited.
}
UNS level 5 segment; lowercase letters, digits, dashes, up to 32 chars.
@foreach (var (id, display) in _lineOptions) { }
@* v3: equipment no longer binds a driver — device I/O is authored in the /raw tree and surfaced here by reference on the Tags tab. The old "Driver instance" select was removed. *@
Unique fleet-wide via ExternalIdReservation.

OPC 40010 identification (optional)
@if (!string.IsNullOrWhiteSpace(_error)) {
@_error
}
} else if (_activeTab == "tags") { @* v3 Batch 3: equipment is reference-only — the Tags tab lists UnsTagReference rows pointing at raw tags. Datatype/access are inherited (read-only) from the backing raw tag; the display-name override is the only per-reference editable field. *@
@if (!string.IsNullOrWhiteSpace(_refError)) {
@_refError
} @if (_refs is null) {

Loading…

} else if (_refs.Count == 0) {

No tag references yet.

} else { @foreach (var r in _refs) { }
Effective nameRaw pathData typeAccess Display-name overrideActions
@r.EffectiveName @r.RawPath @r.DataType @r.AccessLevel
} } else if (_activeTab == "vtags") {
@if (!string.IsNullOrWhiteSpace(_vtagError)) {
@_vtagError
} @if (_vtags is null) {

Loading…

} else if (_vtags.Count == 0) {

No virtual tags yet.

} else { @foreach (var v in _vtags) { }
NameData typeScriptEnabledActions
@v.Name @v.DataType @v.ScriptId @(v.Enabled ? "Yes" : "No")
} } else if (_activeTab == "alarms") {
@if (!string.IsNullOrWhiteSpace(_alarmError)) {
@_alarmError
} @if (_alarms is null) {

Loading…

} else if (_alarms.Count == 0) {

No alarms yet.

} else { @foreach (var a in _alarms) { }
NameTypeSeverityPredicateEnabledActions
@a.Name @a.AlarmType @a.Severity @a.PredicateScriptId @(a.Enabled ? "Yes" : "No")
} } } @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)>(); // --- Tags tab state (v3 reference-only). _refs is null until the tab is first activated (lazy load + // spinner). _overrideEdits carries the per-row editable display-name override, keyed by reference id. --- private IReadOnlyList? _refs; private readonly Dictionary _overrideEdits = new(StringComparer.Ordinal); private string? _refError; private bool _refModalVisible; // --- 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)>(); // --- Alarms tab state. _alarms is null until the tab is first activated. --- private IReadOnlyList? _alarms; private string? _alarmError; private bool _alarmModalVisible; private bool _alarmModalIsNew; private ScriptedAlarmEditDto? _alarmModalExisting; private IReadOnlyList<(string Id, string Display)> _alarmScriptOptions = 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" && _refs is null) { await ReloadReferencesAsync(); } else if (tab == "vtags" && _vtags is null) { await ReloadVirtualTagsAsync(); } else if (tab == "alarms" && _alarms is null) { await ReloadAlarmsAsync(); } } // --- Tags tab handlers (v3 reference-only; the owning equipment is fixed = EquipmentId) --- private async Task ReloadReferencesAsync() { _refs = await Svc.LoadReferencesForEquipmentAsync(EquipmentId!); // Seed the per-row override edit buffer so each row's binds to a live value. _overrideEdits.Clear(); foreach (var r in _refs) { _overrideEdits[r.UnsTagReferenceId] = r.DisplayNameOverride; } } private void OpenAddReference() { _refError = null; _refModalVisible = true; } private async Task OnReferencesCommittedAsync() { _refModalVisible = false; await ReloadReferencesAsync(); } private async Task SaveOverride(EquipmentReferenceRow r) { _refError = null; var edited = _overrideEdits.GetValueOrDefault(r.UnsTagReferenceId); var res = await Svc.SetReferenceOverrideAsync(r.UnsTagReferenceId, edited, r.RowVersion); if (res.Ok) { await ReloadReferencesAsync(); } else { _refError = res.Error; } } private async Task RemoveReference(EquipmentReferenceRow r) { _refError = null; var res = await Svc.RemoveReferenceAsync(r.UnsTagReferenceId, r.RowVersion); if (res.Ok) { await ReloadReferencesAsync(); } else { _refError = res.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; } } // --- Alarms tab handlers (mirror the Tags/Virtual Tags tabs; the owning equipment is fixed = EquipmentId) --- private async Task ReloadAlarmsAsync() { _alarms = await Svc.LoadAlarmsForEquipmentAsync(EquipmentId!); } private async Task OpenAddAlarm() { _alarmError = null; _alarmModalIsNew = true; _alarmModalExisting = null; _alarmScriptOptions = await Svc.LoadScriptsAsync(); _alarmModalVisible = true; } private async Task OpenEditAlarm(string scriptedAlarmId) { _alarmError = null; var dto = await Svc.LoadScriptedAlarmAsync(scriptedAlarmId); if (dto is null) { _alarmError = "That alarm no longer exists; the list was refreshed."; await ReloadAlarmsAsync(); return; } _alarmModalIsNew = false; _alarmModalExisting = dto; _alarmScriptOptions = await Svc.LoadScriptsAsync(); _alarmModalVisible = true; } private async Task OnAlarmSavedAsync() { _alarmModalVisible = false; await ReloadAlarmsAsync(); } private async Task DeleteAlarm(string scriptedAlarmId) { _alarmModalVisible = false; _alarmError = null; // Load the alarm fresh to capture its current RowVersion for the concurrency-guarded delete. var dto = await Svc.LoadScriptedAlarmAsync(scriptedAlarmId); if (dto is null) { await ReloadAlarmsAsync(); return; } var r = await Svc.DeleteScriptedAlarmAsync(scriptedAlarmId, dto.RowVersion); if (r.Ok) { await ReloadAlarmsAsync(); } else { _alarmError = 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. _refs = null; _vtags = null; _alarms = 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; } } else { _form = new FormModel { UnsLineId = LineId ?? "" }; var ctx = await Svc.LoadEquipmentPickContextAsync(LineId); _lineOptions = ctx.Lines; } _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, 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.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. if (string.IsNullOrEmpty(result.CreatedId)) { _error = "Equipment was created but no id was returned."; return; } 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? 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; } }