@page "/admin/connections" @page "/admin/data-connections" @using ScadaLink.Security @using ScadaLink.Commons.Entities.Sites @using ScadaLink.Commons.Interfaces.Repositories @attribute [Authorize(Policy = AuthorizationPolicies.RequireAdmin)] @inject ISiteRepository SiteRepository @inject NavigationManager NavigationManager @inject IDialogService Dialog

Connections

@if (_loading) { } else if (_errorMessage != null) {
@_errorMessage
} else {
@if (!string.IsNullOrWhiteSpace(_searchText) && _matchKeys.Count == 0 && _treeRoots.Count > 0) {

No connections match the filter.

} @{ var labelStyle = IsDimmed(node) ? "opacity: 0.4;" : ""; } @if (node.Kind == DcNodeKind.Site) { @node.Label @node.Children.Count } else { @node.Label @node.Connection!.Protocol } @if (node.Kind == DcNodeKind.Site) { } else { } No sites configured. Add sites under Admin → Sites.
@_connections.Count connection(s) across @_treeRoots.Count site(s).
}
@code { record DcTreeNode(string Key, string Label, DcNodeKind Kind, List Children, int? SiteId = null, DataConnection? Connection = null); enum DcNodeKind { Site, DataConnection } private List _treeRoots = new(); private List _connections = new(); private bool _loading = true; private string? _errorMessage; private TreeView? _tree; private object? _selectedKey; private string _searchText = string.Empty; private HashSet _matchKeys = new(); private ToastNotification _toast = default!; private bool HasSiteSelected => ResolveSelectedSiteId() != null; protected override async Task OnInitializedAsync() { await LoadDataAsync(); } private async Task LoadDataAsync() { _loading = true; _errorMessage = null; try { var sites = await SiteRepository.GetAllSitesAsync(); _connections = (await SiteRepository.GetAllDataConnectionsAsync()).ToList(); var connBySite = _connections.GroupBy(c => c.SiteId).ToDictionary(g => g.Key, g => g.ToList()); _treeRoots = sites.Select(site => new DcTreeNode( Key: $"site-{site.Id}", Label: site.Name, Kind: DcNodeKind.Site, Children: (connBySite.GetValueOrDefault(site.Id) ?? new()) .Select(c => new DcTreeNode( Key: $"conn-{c.Id}", Label: c.Name, Kind: DcNodeKind.DataConnection, Children: new(), SiteId: c.SiteId, Connection: c)) .ToList(), SiteId: site.Id )).ToList(); RebuildMatchKeys(); } catch (Exception ex) { _errorMessage = $"Failed to load data: {ex.Message}"; } _loading = false; } private void OnTreeNodeSelected(object? key) { _selectedKey = key; } private int? ResolveSelectedSiteId() { if (_selectedKey is not string keyStr) return null; foreach (var site in _treeRoots) { if (site.Key == keyStr) return site.SiteId; foreach (var child in site.Children) { if (child.Key == keyStr) return site.SiteId; } } return null; } private void OnAddConnectionClicked() { var sid = ResolveSelectedSiteId(); if (sid == null) return; AddConnectionForSite(sid.Value); } private void AddConnectionForSite(int siteId) { NavigationManager.NavigateTo($"/admin/connections/create?siteId={siteId}"); } private void OnSearchChanged() { RebuildMatchKeys(); } private void RebuildMatchKeys() { _matchKeys.Clear(); if (string.IsNullOrWhiteSpace(_searchText)) return; var q = _searchText.Trim(); foreach (var root in _treeRoots) { SubtreeContainsMatch(root, q); } } private bool SubtreeContainsMatch(DcTreeNode node, string query) { var selfMatch = node.Label.Contains(query, StringComparison.OrdinalIgnoreCase); var childMatch = false; foreach (var child in node.Children) { if (SubtreeContainsMatch(child, query)) childMatch = true; } if (selfMatch || childMatch) _matchKeys.Add(node.Key); return selfMatch || childMatch; } private bool IsDimmed(DcTreeNode node) { if (string.IsNullOrWhiteSpace(_searchText)) return false; return !_matchKeys.Contains(node.Key); } private async Task DeleteConnection(DataConnection conn) { var confirmed = await Dialog.ConfirmAsync( "Delete Connection", $"Delete data connection '{conn.Name}'?", danger: true); if (!confirmed) return; try { await SiteRepository.DeleteDataConnectionAsync(conn.Id); await SiteRepository.SaveChangesAsync(); _toast.ShowSuccess($"Connection '{conn.Name}' deleted."); await LoadDataAsync(); } catch (Exception ex) { _toast.ShowError($"Delete failed: {ex.Message}"); } } }