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 19d7d848..96236d97 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 @@ -138,7 +138,12 @@ else } else if (_activeTab == "tags") { -
+
+ @if (_gateways.Count == 0) + { + No Galaxy gateway in this cluster + } +
@if (!string.IsNullOrWhiteSpace(_tagError)) @@ -157,7 +162,7 @@ else { - + @foreach (var t in _tags) @@ -167,8 +172,22 @@ else + @@ -180,6 +199,10 @@ else + + } else if (_activeTab == "vtags") { @@ -300,6 +323,14 @@ else private TagEditDto? _tagModalExisting; private IReadOnlyList<(string Id, string Display, string DriverType)> _tagDriverOptions = Array.Empty<(string, string, string)>(); + // --- Alias-tag modal state (Galaxy alias = an equipment Tag bound to a GalaxyMxGateway driver). The + // gateways are reloaded alongside _tags so the Add-alias button can disable itself when none exist. --- + private IReadOnlyList<(string DriverInstanceId, string Display, string DriverConfig)> _gateways + = Array.Empty<(string, string, string)>(); + private bool _aliasModalVisible; + private bool _aliasModalIsNew; + private AliasTagEditDto? _aliasModalExisting; + // --- Virtual Tags tab state. _vtags is null until the tab is first activated. --- private IReadOnlyList? _vtags; private string? _vtagError; @@ -339,6 +370,8 @@ else private async Task ReloadTagsAsync() { _tags = await Svc.LoadTagsForEquipmentAsync(EquipmentId!); + // Also refresh the candidate Galaxy gateways so the Add-alias affordance reflects the cluster. + _gateways = await Svc.LoadGalaxyGatewaysForEquipmentAsync(EquipmentId!); } private async Task OpenAddTag() @@ -379,6 +412,33 @@ else else { _tagError = r.Error; } } + // --- Alias-tag handlers (mirror the tag handlers; aliases live in the same _tags list + delete the + // same way, so only the create/edit modal differs — it embeds the Galaxy live-browse picker). --- + + private void OpenAddAlias() + { + _tagError = null; + _aliasModalIsNew = true; + _aliasModalExisting = null; + _aliasModalVisible = true; + } + + private async Task OpenEditAlias(string tagId) + { + _tagError = null; + var dto = await Svc.LoadAliasTagAsync(tagId); + if (dto is null) { _tagError = "That alias no longer exists; the list was refreshed."; await ReloadTagsAsync(); return; } + _aliasModalIsNew = false; + _aliasModalExisting = dto; + _aliasModalVisible = true; + } + + private async Task OnAliasSavedAsync() + { + _aliasModalVisible = false; + await ReloadTagsAsync(); + } + // --- Virtual Tags tab handlers --- private async Task ReloadVirtualTagsAsync() diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/AliasTagModal.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/AliasTagModal.razor new file mode 100644 index 00000000..431ed521 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/AliasTagModal.razor @@ -0,0 +1,246 @@ +@* Create/edit modal for a Galaxy alias tag — an equipment Tag bound to a GalaxyMxGateway driver that + surfaces a Galaxy attribute (its tag_name.AttributeName reference) under a friendly UNS name. A focused + sibling of TagModal: it drops the generic driver/tag-config surface and instead embeds the Galaxy + live-browse picker so the operator picks the Galaxy reference straight into FullName. The host page owns + visibility and supplies the owning equipment id (create) or the loaded AliasTagEditDto (edit), plus the + equipment's candidate Galaxy gateways. On a successful save it raises OnSaved so the host can refresh the + equipment's tags in place. *@ +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Components.Forms +@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers +@using ZB.MOM.WW.OtOpcUa.Configuration.Enums +@inject IUnsTreeService Svc + +@if (Visible) +{ + + +} + +@code { + // Same OPC UA built-in type names the TagModal offers, kept in sync. + private static readonly string[] DataTypes = + ["Boolean", "SByte", "Byte", "Int16", "UInt16", "Int32", "UInt32", + "Int64", "UInt64", "Float", "Double", "String", "DateTime", "Guid", "ByteString"]; + + /// Whether the modal is shown. The host owns this flag. + [Parameter] public bool Visible { get; set; } + + /// true to create a new alias; false to edit . + [Parameter] public bool IsNew { get; set; } + + /// The owning equipment id the created alias binds to (used only on create). + [Parameter] public string? EquipmentId { get; set; } + + /// The alias being edited, when is false. + [Parameter] public AliasTagEditDto? Existing { get; set; } + + /// The candidate Galaxy gateways — scoped to the equipment's cluster by the host — as + /// (DriverInstanceId, Display, DriverConfig) triples. The config feeds the live-browse picker. + [Parameter] public IReadOnlyList<(string DriverInstanceId, string Display, string DriverConfig)> Gateways { get; set; } + = Array.Empty<(string, string, string)>(); + + /// Raised after a successful create/save so the host can refresh the equipment's tags and close. + [Parameter] public EventCallback OnSaved { get; set; } + + /// Raised when the user cancels so the host can close. + [Parameter] public EventCallback OnCancel { get; set; } + + private FormModel _form = new(); + private bool _busy; + private string? _error; + private bool _showPicker; + + // The selected gateway's DriverConfig JSON — fed to the Galaxy picker so it browses the right gateway. + private string _selectedGatewayConfig = "{}"; + + // Tracks which open this modal last loaded for, so unrelated Blazor Server re-renders don't rebuild + // _form and clobber in-progress edits. Null while closed. + private string? _loadedKey; + + protected override void OnParametersSet() + { + if (!Visible) + { + _loadedKey = null; // closed → next open reloads fresh + return; + } + + // Guard against unrelated re-renders. In Blazor Server any live-status push re-invokes + // OnParametersSet; without this the rebuild below would silently discard whatever the operator + // has typed. Only rebuild when the modal OPENS or the target entity CHANGES. + var key = IsNew ? "" : Existing?.TagId; + if (key == _loadedKey) return; // same open, re-render → preserve in-progress form edits + _loadedKey = key; + + if (IsNew) + { + _form = new FormModel(); + // Auto-select when exactly one gateway, so the picker can browse without an extra click. + if (Gateways.Count == 1) { _form.DriverInstanceId = Gateways[0].DriverInstanceId; } + } + else if (Existing is not null) + { + _form = new FormModel + { + Name = Existing.Name, + DriverInstanceId = Existing.DriverInstanceId, + DataType = Existing.DataType, + AccessLevel = Existing.AccessLevel, + FullName = Existing.FullName, + }; + } + _error = null; + _showPicker = false; + SyncSelectedGatewayConfig(); + } + + // Keeps the picker's browse config aligned with the chosen gateway whenever the operator switches it. + private void OnGatewayChanged() => SyncSelectedGatewayConfig(); + + private void SyncSelectedGatewayConfig() + { + var match = Gateways.FirstOrDefault(g => g.DriverInstanceId == _form.DriverInstanceId); + _selectedGatewayConfig = string.IsNullOrEmpty(match.DriverConfig) ? "{}" : match.DriverConfig; + } + + private void OnAddressPicked(string address) => _form.FullName = address; + + private async Task SaveAsync() + { + // Client validation: Name, FullName, and a chosen gateway are all required for a usable alias. + if (string.IsNullOrWhiteSpace(_form.Name)) { _error = "Name is required."; return; } + if (string.IsNullOrWhiteSpace(_form.DriverInstanceId)) { _error = "Pick a Galaxy gateway."; return; } + if (string.IsNullOrWhiteSpace(_form.FullName)) { _error = "A Galaxy reference is required — type it or browse to pick one."; return; } + + _busy = true; + _error = null; + try + { + // On create the TagId is system-minted (the alias has no operator-typed id field), matching the + // service's own NewTagId() shape: "TAG-" + 12 hex chars. Ignored on update. + var tagId = IsNew ? $"TAG-{Guid.NewGuid().ToString("N")[..12]}" : Existing!.TagId; + var input = new AliasTagInput(tagId, _form.Name, _form.DriverInstanceId, _form.DataType, _form.AccessLevel, _form.FullName); + + var result = IsNew + ? await Svc.CreateAliasTagAsync(EquipmentId!, input) + : await Svc.UpdateAliasTagAsync(Existing!.TagId, 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] public string Name { get; set; } = ""; + [Required] public string DriverInstanceId { get; set; } = ""; + public string DataType { get; set; } = "Float"; + public TagAccessLevel AccessLevel { get; set; } = TagAccessLevel.Read; + [Required] public string FullName { get; set; } = ""; + } +}
NameDriverData typeAccessActions
NameDriverData typeAccessSourceActions
@t.DriverInstanceId @t.DataType @t.AccessLevel + @if (t.IsAlias) + { + alias + @t.Source + } + - + @if (t.IsAlias) + { + + } + else + { + + }