@page "/uns" @attribute [Microsoft.AspNetCore.Authorization.Authorize] @rendermode RenderMode.InteractiveServer @using ZB.MOM.WW.OtOpcUa.AdminUI.Uns @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Uns @inject IUnsTreeService Svc UNS

UNS

Changes apply on the next deployment.
Unified namespace
@if (_loading) {
Loading…
} else if (_roots.Count == 0) {
No clusters yet.
} else {
}
@if (_confirmNode is not null) { } @code { private IReadOnlyList _roots = Array.Empty(); private string? _filter; private bool _loading = true; // Guards the async modal openers (HandleAddChild/HandleAddVirtualTag/HandleEdit) so a rapid // double-action can't race two service loads into the same modal state. private bool _modalBusy; // --- Area modal state --- private bool _areaModalVisible; private bool _areaModalIsNew; private string? _areaModalClusterId; private AreaEditDto? _areaModalExisting; // --- Line modal state --- private bool _lineModalVisible; private bool _lineModalIsNew; private string? _lineModalAreaId; private LineEditDto? _lineModalExisting; private IReadOnlyList<(string Id, string Display)> _lineModalAreaOptions = Array.Empty<(string, string)>(); // --- Equipment modal state --- private bool _equipmentModalVisible; private bool _equipmentModalIsNew; private string? _equipmentModalLineId; private EquipmentEditDto? _equipmentModalExisting; private IReadOnlyList<(string Id, string Display)> _equipmentModalLineOptions = Array.Empty<(string, string)>(); private IReadOnlyList<(string Id, string Display)> _equipmentModalDriverOptions = Array.Empty<(string, string)>(); // --- Tag modal state --- private bool _tagModalVisible; private bool _tagModalIsNew; private string? _tagModalEquipmentId; private TagEditDto? _tagModalExisting; private IReadOnlyList<(string Id, string Display, string DriverType)> _tagModalDriverOptions = Array.Empty<(string, string, string)>(); // --- Virtual-tag modal state --- private bool _vtagModalVisible; private bool _vtagModalIsNew; private string? _vtagModalEquipmentId; private VirtualTagEditDto? _vtagModalExisting; private IReadOnlyList<(string Id, string Display)> _vtagModalScriptOptions = Array.Empty<(string, string)>(); // --- Import-equipment-CSV modal state --- private bool _importModalVisible; // --- Owning equipment to refresh in place after a tag/virtual-tag mutation --- private string? _childRefreshEquipmentId; // --- Delete-confirm state --- private UnsNode? _confirmNode; private bool _confirmBusy; private string? _confirmError; /// The served-by cluster options for the AreaModal, derived from the loaded tree. private IReadOnlyList<(string Id, string Display)> ClusterOptions => _roots .SelectMany(ent => ent.Children) .Where(n => n.Kind == UnsNodeKind.Cluster && n.EntityId is not null) .Select(n => (n.EntityId!, n.DisplayName)) .ToList(); protected override async Task OnInitializedAsync() { _roots = await Svc.LoadStructureAsync(); _loading = false; } /// Returns the (Id, Display) area options inside a single cluster, for the line picker. private IReadOnlyList<(string Id, string Display)> AreaOptionsForCluster(string? clusterId) => _roots .SelectMany(ent => ent.Children) .Where(c => c.Kind == UnsNodeKind.Cluster && c.ClusterId == clusterId) .SelectMany(c => c.Children) .Where(a => a.Kind == UnsNodeKind.Area && a.EntityId is not null) .Select(a => (a.EntityId!, a.DisplayName)) .ToList(); /// Returns the (Id, Display) line options inside a single cluster, for the equipment picker. private IReadOnlyList<(string Id, string Display)> LinesForCluster(string? clusterId) => _roots .SelectMany(ent => ent.Children) .Where(c => c.Kind == UnsNodeKind.Cluster && c.ClusterId == clusterId) .SelectMany(c => c.Children) .Where(a => a.Kind == UnsNodeKind.Area) .SelectMany(a => a.Children) .Where(l => l.Kind == UnsNodeKind.Line && l.EntityId is not null) .Select(l => (l.EntityId!, l.DisplayName)) .ToList(); /// /// Toggles a node's expansion. For equipment nodes whose children have not yet /// been loaded, lazily fetches the tag/virtual-tag leaves on first expand. /// private async Task ToggleAsync(UnsNode node) { if (node.Loading) return; node.Expanded = !node.Expanded; if (node.Kind == UnsNodeKind.Equipment && node.Expanded && !node.Loaded) { node.Error = null; node.Loading = true; StateHasChanged(); try { var kids = await Svc.LoadEquipmentChildrenAsync(node.EntityId!); node.Children.Clear(); node.Children.AddRange(kids); node.Loaded = true; } catch (Exception ex) { node.Error = ex.Message; } finally { node.Loading = false; StateHasChanged(); } } } /// /// Opens the create modal for a node's primary child: a cluster gets a new area; an area gets a /// new line scoped to its cluster; a line gets a new equipment scoped to its cluster; an equipment /// gets a new tag scoped to its candidate drivers. /// private async Task HandleAddChild(UnsNode node) { if (_modalBusy) { return; } _modalBusy = true; try { CloseModals(); switch (node.Kind) { case UnsNodeKind.Cluster: _areaModalIsNew = true; _areaModalExisting = null; _areaModalClusterId = node.ClusterId ?? node.EntityId; _areaModalVisible = true; break; case UnsNodeKind.Area: _lineModalIsNew = true; _lineModalExisting = null; _lineModalAreaId = node.EntityId; _lineModalAreaOptions = AreaOptionsForCluster(node.ClusterId); _lineModalVisible = true; break; case UnsNodeKind.Line: _equipmentModalIsNew = true; _equipmentModalExisting = null; _equipmentModalLineId = node.EntityId; _equipmentModalLineOptions = LinesForCluster(node.ClusterId); _equipmentModalDriverOptions = await Svc.LoadDriversForClusterAsync(node.ClusterId!); _equipmentModalVisible = true; break; case UnsNodeKind.Equipment: _tagModalIsNew = true; _tagModalExisting = null; _tagModalEquipmentId = node.EntityId; _childRefreshEquipmentId = node.EntityId; _tagModalDriverOptions = await Svc.LoadTagDriversForEquipmentAsync(node.EntityId!); _tagModalVisible = true; break; } } finally { _modalBusy = false; } } /// Opens the create modal for a new virtual tag scoped to the clicked equipment. private async Task HandleAddVirtualTag(UnsNode node) { if (_modalBusy) { return; } _modalBusy = true; try { CloseModals(); _vtagModalIsNew = true; _vtagModalExisting = null; _vtagModalEquipmentId = node.EntityId; _childRefreshEquipmentId = node.EntityId; _vtagModalScriptOptions = await Svc.LoadScriptsAsync(); _vtagModalVisible = true; } finally { _modalBusy = false; } } /// /// Opens the edit modal for the clicked node, loading the entity first to prefill the form and /// capture its RowVersion. Tag/VirtualTag edits also stash the owning equipment id so a successful /// save can refresh just that equipment's children in place. /// private async Task HandleEdit(UnsNode node) { if (_modalBusy) { return; } _modalBusy = true; try { CloseModals(); switch (node.Kind) { case UnsNodeKind.Area: var area = await Svc.LoadAreaAsync(node.EntityId!); if (area is null) { return; } _areaModalIsNew = false; _areaModalExisting = area; _areaModalClusterId = area.ClusterId; _areaModalVisible = true; break; case UnsNodeKind.Line: var line = await Svc.LoadLineAsync(node.EntityId!); if (line is null) { return; } _lineModalIsNew = false; _lineModalExisting = line; _lineModalAreaId = line.UnsAreaId; _lineModalAreaOptions = AreaOptionsForCluster(node.ClusterId); _lineModalVisible = true; break; case UnsNodeKind.Equipment: var equipment = await Svc.LoadEquipmentAsync(node.EntityId!); if (equipment is null) { return; } _equipmentModalIsNew = false; _equipmentModalExisting = equipment; _equipmentModalLineId = equipment.UnsLineId; _equipmentModalLineOptions = LinesForCluster(node.ClusterId); _equipmentModalDriverOptions = await Svc.LoadDriversForClusterAsync(node.ClusterId!); _equipmentModalVisible = true; break; case UnsNodeKind.Tag: var tag = await Svc.LoadTagAsync(node.EntityId!); if (tag is null) { return; } _tagModalIsNew = false; _tagModalExisting = tag; _tagModalEquipmentId = tag.EquipmentId; _childRefreshEquipmentId = tag.EquipmentId; _tagModalDriverOptions = await Svc.LoadTagDriversForEquipmentAsync(tag.EquipmentId); _tagModalVisible = true; break; case UnsNodeKind.VirtualTag: var vtag = await Svc.LoadVirtualTagAsync(node.EntityId!); if (vtag is null) { return; } _vtagModalIsNew = false; _vtagModalExisting = vtag; _vtagModalEquipmentId = vtag.EquipmentId; _childRefreshEquipmentId = vtag.EquipmentId; _vtagModalScriptOptions = await Svc.LoadScriptsAsync(); _vtagModalVisible = true; break; } } finally { _modalBusy = false; } } /// Opens the bulk equipment-CSV import modal. private void OpenImportModal() { CloseModals(); _importModalVisible = true; } /// /// Handles the import modal's close after a run. A bulk import can add equipment across many /// lines and clusters, so the whole structural tree is reloaded rather than refreshed in place. /// private async Task OnImportedAsync() { _roots = await Svc.LoadStructureAsync(); CloseModals(); StateHasChanged(); } /// Opens the delete-confirm modal for a node, stashing it as the pending target. private void HandleDelete(UnsNode node) { CloseModals(); _confirmNode = node; } /// /// Performs the pending delete. Loads the entity's RowVersion first, then dispatches on Kind. /// Area/Line/Equipment trigger a full structural reload on success; Tag/VirtualTag refresh only the /// owning equipment's children in place so the rest of the user's expansion is preserved. /// private async Task ConfirmDeleteAsync() { if (_confirmNode is null) { return; } _confirmBusy = true; _confirmError = null; try { var node = _confirmNode; UnsMutationResult result; // Tag/VirtualTag deletes refresh just the owning equipment's children rather than the // whole tree, so they're handled separately from the structural Area/Line/Equipment path. switch (node.Kind) { case UnsNodeKind.Tag: var tag = await Svc.LoadTagAsync(node.EntityId!); if (tag is null) { await ReloadAndCloseAsync(); return; } result = await Svc.DeleteTagAsync(node.EntityId!, tag.RowVersion); if (result.Ok) { await RefreshEquipmentChildrenAsync(tag.EquipmentId); CloseModals(); StateHasChanged(); } else { _confirmError = result.Error; } return; case UnsNodeKind.VirtualTag: var vtag = await Svc.LoadVirtualTagAsync(node.EntityId!); if (vtag is null) { await ReloadAndCloseAsync(); return; } result = await Svc.DeleteVirtualTagAsync(node.EntityId!, vtag.RowVersion); if (result.Ok) { await RefreshEquipmentChildrenAsync(vtag.EquipmentId); CloseModals(); StateHasChanged(); } else { _confirmError = result.Error; } return; case UnsNodeKind.Area: var area = await Svc.LoadAreaAsync(node.EntityId!); if (area is null) { await ReloadAndCloseAsync(); return; } result = await Svc.DeleteAreaAsync(node.EntityId!, area.RowVersion); break; case UnsNodeKind.Line: var line = await Svc.LoadLineAsync(node.EntityId!); if (line is null) { await ReloadAndCloseAsync(); return; } result = await Svc.DeleteLineAsync(node.EntityId!, line.RowVersion); break; case UnsNodeKind.Equipment: var equipment = await Svc.LoadEquipmentAsync(node.EntityId!); if (equipment is null) { await ReloadAndCloseAsync(); return; } result = await Svc.DeleteEquipmentAsync(node.EntityId!, equipment.RowVersion); break; default: // Enterprise/Cluster have no delete button, so this branch is unreachable in practice. result = new UnsMutationResult(false, "Delete for this node kind is not yet available."); break; } if (result.Ok) { await ReloadAndCloseAsync(); } else { _confirmError = result.Error; } } finally { _confirmBusy = false; } } /// Reloads the tree after a successful modal save and closes any open modal. private async Task OnModalSavedAsync() { _roots = await Svc.LoadStructureAsync(); CloseModals(); StateHasChanged(); } /// /// Handles a successful Tag/VirtualTag modal save by refreshing only the owning equipment's children /// in place — never a full structural reload, which would collapse the user's expansion. /// private async Task OnEquipmentChildModalSavedAsync() { if (_childRefreshEquipmentId is not null) { await RefreshEquipmentChildrenAsync(_childRefreshEquipmentId); } CloseModals(); StateHasChanged(); } /// /// Reloads a single equipment node's tag/virtual-tag children in place, leaving the rest of the tree /// (and the user's expansion) untouched. Falls back to a full structural reload only if the node /// can no longer be found in the current tree. Either branch only mutates state — the caller is /// responsible for calling StateHasChanged() afterwards (every current caller does). /// private async Task RefreshEquipmentChildrenAsync(string equipmentId) { var node = FindEquipmentNode(equipmentId); if (node is null) { // Fallback: the equipment node is no longer in the current tree — reload the whole structure. _roots = await Svc.LoadStructureAsync(); return; } var kids = await Svc.LoadEquipmentChildrenAsync(equipmentId); node.Children.Clear(); node.Children.AddRange(kids); node.ChildCount = node.Children.Count; node.Loaded = true; node.Expanded = true; } /// Recursively walks the current tree for the Equipment node with the given id, or null. private UnsNode? FindEquipmentNode(string equipmentId) { foreach (var root in _roots) { var found = FindEquipmentNode(root, equipmentId); if (found is not null) { return found; } } return null; } private static UnsNode? FindEquipmentNode(UnsNode node, string equipmentId) { if (node.Kind == UnsNodeKind.Equipment && node.EntityId == equipmentId) { return node; } foreach (var child in node.Children) { var found = FindEquipmentNode(child, equipmentId); if (found is not null) { return found; } } return null; } /// Reloads the tree after a successful delete and closes the confirm modal. private async Task ReloadAndCloseAsync() { _roots = await Svc.LoadStructureAsync(); CloseModals(); StateHasChanged(); } /// Closes every modal and clears its transient state. private void CloseModals() { _areaModalVisible = false; _areaModalIsNew = false; _areaModalClusterId = null; _areaModalExisting = null; _lineModalVisible = false; _lineModalIsNew = false; _lineModalAreaId = null; _lineModalExisting = null; _lineModalAreaOptions = Array.Empty<(string, string)>(); _equipmentModalVisible = false; _equipmentModalIsNew = false; _equipmentModalLineId = null; _equipmentModalExisting = null; _equipmentModalLineOptions = Array.Empty<(string, string)>(); _equipmentModalDriverOptions = Array.Empty<(string, string)>(); _tagModalVisible = false; _tagModalIsNew = false; _tagModalEquipmentId = null; _tagModalExisting = null; _tagModalDriverOptions = Array.Empty<(string, string, string)>(); _vtagModalVisible = false; _vtagModalIsNew = false; _vtagModalEquipmentId = null; _vtagModalExisting = null; _vtagModalScriptOptions = Array.Empty<(string, string)>(); _importModalVisible = false; _childRefreshEquipmentId = null; _confirmNode = null; _confirmError = null; } /// /// Expands every structural node (Enterprise/Cluster/Area/Line). Equipment nodes /// are intentionally left collapsed because expanding them would trigger lazy loads. /// private void ExpandAll() { foreach (var root in _roots) { ExpandStructural(root); } } private static void ExpandStructural(UnsNode node) { if (node.Kind is UnsNodeKind.Enterprise or UnsNodeKind.Cluster or UnsNodeKind.Area or UnsNodeKind.Line) { node.Expanded = true; } foreach (var child in node.Children) { ExpandStructural(child); } } /// Collapses every node in the tree. private void CollapseAll() { foreach (var root in _roots) { CollapseNode(root); } } private static void CollapseNode(UnsNode node) { node.Expanded = false; foreach (var child in node.Children) { CollapseNode(child); } } }