diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor index d7407835..75c7dd69 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor @@ -9,6 +9,7 @@ @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 @@ -31,8 +32,8 @@ else { @@ -135,8 +136,96 @@ else } - else if (_activeTab == "tags") {

Tags tab — wired in a later task.

} - else if (_activeTab == "vtags") {

Virtual Tags tab — wired in a later task.

} + else if (_activeTab == "tags") + { +
+ +
+ @if (!string.IsNullOrWhiteSpace(_tagError)) + { +
@_tagError
+ } + @if (_tags is null) + { +

Loading…

+ } + else if (_tags.Count == 0) + { +

No tags yet.

+ } + else + { + + + + + + @foreach (var t in _tags) + { + + + + + + + + } + +
NameDriverData typeAccessActions
@t.Name@t.DriverInstanceId@t.DataType@t.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") {

Alarms tab — wired in a later task.

} } @@ -158,12 +247,134 @@ else 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() + { + _tagModalIsNew = true; + _tagModalExisting = null; + _tagDriverOptions = await Svc.LoadTagDriversForEquipmentAsync(EquipmentId!); + _tagModalVisible = true; + } + + private async Task OpenEditTag(string tagId) + { + var dto = await Svc.LoadTagAsync(tagId); + if (dto is null) { 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) + { + _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() + { + _vtagModalIsNew = true; + _vtagModalExisting = null; + _vtagScriptOptions = await Svc.LoadScriptsAsync(); + _vtagModalVisible = true; + } + + private async Task OpenEditVirtualTag(string vtagId) + { + var dto = await Svc.LoadVirtualTagAsync(vtagId); + if (dto is null) { 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) + { + _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!);