672 lines
25 KiB
Plaintext
672 lines
25 KiB
Plaintext
@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
|
|
|
|
<PageTitle>UNS</PageTitle>
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h4 class="mb-0">UNS</h4>
|
|
<span class="text-muted small">Changes apply on the next deployment.</span>
|
|
</div>
|
|
|
|
<section class="panel rise" style="animation-delay:.02s">
|
|
<div class="panel-head d-flex justify-content-between align-items-center flex-wrap gap-2">
|
|
<span>Unified namespace</span>
|
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
<input type="text" class="form-control form-control-sm" style="max-width:240px"
|
|
placeholder="Filter by name (substring)…"
|
|
@bind="_filter" @bind:event="oninput" />
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="ExpandAll">Expand all</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="CollapseAll">Collapse all</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" @onclick="OpenImportModal">Import equipment CSV</button>
|
|
</div>
|
|
</div>
|
|
|
|
@if (_loading)
|
|
{
|
|
<div style="padding:1rem" class="text-muted">
|
|
<span class="spinner-border spinner-border-sm me-1"></span>Loading…
|
|
</div>
|
|
}
|
|
else if (_roots.Count == 0)
|
|
{
|
|
<div style="padding:1rem" class="text-muted">No clusters yet.</div>
|
|
}
|
|
else
|
|
{
|
|
<div style="padding:.5rem 1rem">
|
|
<UnsTree Roots="_roots" Filter="@_filter"
|
|
OnToggleExpand="ToggleAsync"
|
|
OnAddChild="HandleAddChild"
|
|
OnAddVirtualTag="HandleAddVirtualTag"
|
|
OnEdit="HandleEdit"
|
|
OnDelete="HandleDelete" />
|
|
</div>
|
|
}
|
|
</section>
|
|
|
|
<AreaModal Visible="_areaModalVisible"
|
|
IsNew="_areaModalIsNew"
|
|
ClusterId="@_areaModalClusterId"
|
|
Existing="_areaModalExisting"
|
|
Clusters="ClusterOptions"
|
|
OnSaved="OnModalSavedAsync"
|
|
OnCancel="CloseModals" />
|
|
|
|
<LineModal Visible="_lineModalVisible"
|
|
IsNew="_lineModalIsNew"
|
|
UnsAreaId="@_lineModalAreaId"
|
|
Existing="_lineModalExisting"
|
|
Areas="_lineModalAreaOptions"
|
|
OnSaved="OnModalSavedAsync"
|
|
OnCancel="CloseModals" />
|
|
|
|
<EquipmentModal Visible="_equipmentModalVisible"
|
|
IsNew="_equipmentModalIsNew"
|
|
UnsLineId="@_equipmentModalLineId"
|
|
Existing="_equipmentModalExisting"
|
|
Lines="_equipmentModalLineOptions"
|
|
Drivers="_equipmentModalDriverOptions"
|
|
OnSaved="OnModalSavedAsync"
|
|
OnCancel="CloseModals" />
|
|
|
|
<TagModal Visible="_tagModalVisible"
|
|
IsNew="_tagModalIsNew"
|
|
EquipmentId="@_tagModalEquipmentId"
|
|
Existing="_tagModalExisting"
|
|
Drivers="_tagModalDriverOptions"
|
|
OnSaved="OnEquipmentChildModalSavedAsync"
|
|
OnCancel="CloseModals" />
|
|
|
|
<VirtualTagModal Visible="_vtagModalVisible"
|
|
IsNew="_vtagModalIsNew"
|
|
EquipmentId="@_vtagModalEquipmentId"
|
|
Existing="_vtagModalExisting"
|
|
Scripts="_vtagModalScriptOptions"
|
|
OnSaved="OnEquipmentChildModalSavedAsync"
|
|
OnCancel="CloseModals" />
|
|
|
|
<ImportEquipmentModal Visible="_importModalVisible"
|
|
OnImported="OnImportedAsync"
|
|
OnCancel="CloseModals" />
|
|
|
|
@if (_confirmNode is not null)
|
|
{
|
|
<div class="modal-backdrop fade show" style="display:block"></div>
|
|
<div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
|
|
<div class="modal-dialog" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Delete @_confirmNode.Kind</h5>
|
|
<button type="button" class="btn-close" aria-label="Close" @onclick="CloseModals"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Delete <span class="mono">@_confirmNode.DisplayName</span>? This cannot be undone.</p>
|
|
@if (!string.IsNullOrWhiteSpace(_confirmError))
|
|
{
|
|
<div class="text-danger small mt-2">@_confirmError</div>
|
|
}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" @onclick="CloseModals" disabled="@_confirmBusy">Cancel</button>
|
|
<button type="button" class="btn btn-danger" @onclick="ConfirmDeleteAsync" disabled="@_confirmBusy">
|
|
@if (_confirmBusy) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
private IReadOnlyList<UnsNode> _roots = Array.Empty<UnsNode>();
|
|
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;
|
|
|
|
/// <summary>The served-by cluster options for the AreaModal, derived from the loaded tree.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Returns the <c>(Id, Display)</c> area options inside a single cluster, for the line picker.</summary>
|
|
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();
|
|
|
|
/// <summary>Returns the <c>(Id, Display)</c> line options inside a single cluster, for the equipment picker.</summary>
|
|
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();
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Opens the create modal for a new virtual tag scoped to the clicked equipment.</summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Opens the bulk equipment-CSV import modal.</summary>
|
|
private void OpenImportModal()
|
|
{
|
|
CloseModals();
|
|
_importModalVisible = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private async Task OnImportedAsync()
|
|
{
|
|
_roots = await Svc.LoadStructureAsync();
|
|
CloseModals();
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>Opens the delete-confirm modal for a node, stashing it as the pending target.</summary>
|
|
private void HandleDelete(UnsNode node)
|
|
{
|
|
CloseModals();
|
|
_confirmNode = node;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Reloads the tree after a successful modal save and closes any open modal.</summary>
|
|
private async Task OnModalSavedAsync()
|
|
{
|
|
_roots = await Svc.LoadStructureAsync();
|
|
CloseModals();
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private async Task OnEquipmentChildModalSavedAsync()
|
|
{
|
|
if (_childRefreshEquipmentId is not null)
|
|
{
|
|
await RefreshEquipmentChildrenAsync(_childRefreshEquipmentId);
|
|
}
|
|
CloseModals();
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Recursively walks the current tree for the Equipment node with the given id, or null.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Reloads the tree after a successful delete and closes the confirm modal.</summary>
|
|
private async Task ReloadAndCloseAsync()
|
|
{
|
|
_roots = await Svc.LoadStructureAsync();
|
|
CloseModals();
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>Closes every modal and clears its transient state.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expands every structural node (Enterprise/Cluster/Area/Line). Equipment nodes
|
|
/// are intentionally left collapsed because expanding them would trigger lazy loads.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>Collapses every node in the tree.</summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|