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
+ {
+
+
+ | Name | Driver | Data type | Access | Actions |
+
+
+ @foreach (var t in _tags)
+ {
+
+ | @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
+ {
+
+
+ | Name | Data type | Script | Enabled | Actions |
+
+
+ @foreach (var v in _vtags)
+ {
+
+ | @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!);