feat(transport-ui): export wizard site/instance selection (M8 E1)

This commit is contained in:
Joseph Doherty
2026-06-18 06:26:36 -04:00
parent bdbf5cdab0
commit d0b38ad726
3 changed files with 314 additions and 5 deletions
@@ -6,8 +6,10 @@ using Microsoft.JSInterop;
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
@@ -51,6 +53,7 @@ public partial class TransportExport : ComponentBase
[Inject] private IExternalSystemRepository ExternalRepo { get; set; } = default!;
[Inject] private INotificationRepository NotificationRepo { get; set; } = default!;
[Inject] private IInboundApiRepository InboundApiRepo { get; set; } = default!;
[Inject] private ISiteRepository SiteRepo { get; set; } = default!;
[Inject] private DependencyResolver DepResolver { get; set; } = default!;
[Inject] private IJSRuntime JS { get; set; } = default!;
[Inject] private AuthenticationStateProvider Auth { get; set; } = default!;
@@ -71,6 +74,11 @@ public partial class TransportExport : ComponentBase
private List<SmtpConfiguration> _smtpConfigs = new();
// Inbound API keys are not transported between environments (re-arch C4); only methods.
private List<ApiMethod> _apiMethods = new();
// M8 (E1): site/instance-scoped export. Sites are listed flat; each site's
// instances hang off it (loaded eagerly via ISiteRepository.GetInstancesBySiteIdAsync)
// so the operator can pick a whole site or drill into individual instances.
private List<Site> _sites = new();
private readonly Dictionary<int, List<Instance>> _instancesBySiteId = new();
// ---- Step 1: selection state ----
// TemplateFolderTree uses string keys ("t:{id}", "f:{id}") via TemplateTreeNode.Key.
@@ -84,6 +92,12 @@ public partial class TransportExport : ComponentBase
private readonly HashSet<int> _selectedSmtpConfigs = new();
// No _selectedApiKeys: inbound API keys are not transported (re-arch C4).
private readonly HashSet<int> _selectedApiMethods = new();
// M8 (E1): site/instance selection backed by entity primary keys, matching the
// DependencyResolver's SiteIds/InstanceIds (NOT names — the resolver fetches by id).
private readonly HashSet<int> _selectedSites = new();
private readonly HashSet<int> _selectedInstances = new();
// Which site rows are expanded to reveal their instance checkboxes (UI-only).
private readonly HashSet<int> _expandedSites = new();
private string _filter = string.Empty;
private bool _includeDependencies = true;
@@ -126,6 +140,22 @@ public partial class TransportExport : ComponentBase
_smtpConfigs = (await NotificationRepo.GetAllSmtpConfigurationsAsync()).ToList();
// Inbound API keys are not transported (re-arch C4) — only methods are loaded.
_apiMethods = (await InboundApiRepo.GetAllApiMethodsAsync()).ToList();
// M8 (E1): sites + their instances for site/instance-scoped export. Each
// site's instances are loaded eagerly so the expandable picker has them
// without a per-click round-trip; sites are ordered by identifier to match
// the bundle's deterministic site ordering.
_sites = (await SiteRepo.GetAllSitesAsync())
.OrderBy(s => s.SiteIdentifier, StringComparer.OrdinalIgnoreCase)
.ToList();
_instancesBySiteId.Clear();
foreach (var site in _sites)
{
var instances = (await SiteRepo.GetInstancesBySiteIdAsync(site.Id))
.OrderBy(i => i.UniqueName, StringComparer.OrdinalIgnoreCase)
.ToList();
_instancesBySiteId[site.Id] = instances;
}
}
catch (Exception ex)
{
@@ -169,7 +199,9 @@ public partial class TransportExport : ComponentBase
|| _selectedDbConnections.Count > 0
|| _selectedNotificationLists.Count > 0
|| _selectedSmtpConfigs.Count > 0
|| _selectedApiMethods.Count > 0;
|| _selectedApiMethods.Count > 0
|| _selectedSites.Count > 0
|| _selectedInstances.Count > 0;
private bool PassphraseValid =>
!string.IsNullOrEmpty(_passphrase)
@@ -207,7 +239,15 @@ public partial class TransportExport : ComponentBase
SmtpConfigurationIds: _selectedSmtpConfigs.ToList(),
// Inbound API keys are not transported (re-arch C4) — methods only.
ApiMethodIds: _selectedApiMethods.ToList(),
IncludeDependencies: _includeDependencies);
IncludeDependencies: _includeDependencies)
{
// M8 (E1): site/instance ids feed the resolver's SiteIds/InstanceIds.
// Set via init-only properties so the positional ctor stays the documented
// additive shape; the resolver dedups a selected instance against its
// already-selected owning site.
SiteIds = _selectedSites.ToList(),
InstanceIds = _selectedInstances.ToList(),
};
}
private async Task GoToReviewAsync()
@@ -396,6 +436,9 @@ public partial class TransportExport : ComponentBase
_selectedNotificationLists.Clear();
_selectedSmtpConfigs.Clear();
_selectedApiMethods.Clear();
_selectedSites.Clear();
_selectedInstances.Clear();
_expandedSites.Clear();
_filter = string.Empty;
_includeDependencies = true;
_resolved = null;
@@ -420,6 +463,43 @@ public partial class TransportExport : ComponentBase
else set.Remove(id);
}
// ---- Step 1 site/instance helpers (M8 E1) ----
/// <summary>Instances loaded for a site, or an empty list when the site has none.</summary>
private IReadOnlyList<Instance> InstancesFor(int siteId) =>
_instancesBySiteId.TryGetValue(siteId, out var list) ? list : Array.Empty<Instance>();
/// <summary>Expand/collapse a site row's nested instance list (UI-only state).</summary>
private void ToggleSiteExpanded(int siteId)
{
if (!_expandedSites.Remove(siteId))
{
_expandedSites.Add(siteId);
}
}
/// <summary>
/// Toggle a whole site. Selecting a site lets the resolver pull its instances,
/// so on select we clear any redundant per-instance ticks for that site; on
/// deselect we leave individual instances untouched (the operator may still
/// want a subset). Matches the resolver's dedup semantics.
/// </summary>
private void ToggleSite(int siteId, bool value)
{
if (value)
{
_selectedSites.Add(siteId);
foreach (var instance in InstancesFor(siteId))
{
_selectedInstances.Remove(instance.Id);
}
}
else
{
_selectedSites.Remove(siteId);
}
}
// ---- Step 2 grouping helpers ----
/// <summary>