From bc4fc97652bd4108e5e3bd191980a568f983bf80 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 22 Mar 2026 15:58:32 -0400 Subject: [PATCH] refactor(ui): extract instance bindings and overrides to dedicated Configure page Move connection bindings, attribute overrides, and area assignment from inline expandable rows on the Instances table to a separate page at /deployment/instances/{id}/configure for a cleaner, less cramped UX. --- .../Pages/Deployment/InstanceConfigure.razor | 367 ++++++++++++++++++ .../Pages/Deployment/Instances.razor | 281 +------------- 2 files changed, 369 insertions(+), 279 deletions(-) create mode 100644 src/ScadaLink.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor diff --git a/src/ScadaLink.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor b/src/ScadaLink.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor new file mode 100644 index 0000000..1bfa7b5 --- /dev/null +++ b/src/ScadaLink.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor @@ -0,0 +1,367 @@ +@page "/deployment/instances/{Id:int}/configure" +@using ScadaLink.Security +@using ScadaLink.Commons.Entities.Instances +@using ScadaLink.Commons.Entities.Sites +@using ScadaLink.Commons.Entities.Templates +@using ScadaLink.Commons.Interfaces.Repositories +@using ScadaLink.Commons.Types.Enums +@using ScadaLink.TemplateEngine.Services +@attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)] +@inject ITemplateEngineRepository TemplateEngineRepository +@inject ISiteRepository SiteRepository +@inject InstanceService InstanceService +@inject AuthenticationStateProvider AuthStateProvider +@inject NavigationManager NavigationManager + +
+
+ +

Configure Instance

+
+ + + + @if (_loading) + { + + } + else if (_errorMessage != null) + { +
@_errorMessage
+ } + else if (_instance != null) + { + @* Instance Identity *@ +
+
+
+
+ Instance +
@_instance.UniqueName
+
+
+ Template +
@_templateName
+
+
+ Site +
@_siteName
+
+
+ Status +
@_instance.State
+
+
+ Area +
@(_instance.AreaId.HasValue ? _areaName : "—")
+
+
+
+
+ + @* Connection Bindings *@ +
+
+ Connection Bindings + @if (_bindingDataSourceAttrs.Count > 0 && _siteConnections.Count > 0) + { +
+ + +
+ } +
+
+ @if (_bindingDataSourceAttrs.Count == 0) + { +

No data-sourced attributes in this template.

+ } + else + { + + + + + + + + + + @foreach (var attr in _bindingDataSourceAttrs) + { + + + + + + } + +
AttributeTag PathConnection
@attr.Name@attr.DataSourceReference + +
+
+ +
+ } +
+
+ + @* Attribute Overrides *@ +
+
+ Attribute Overrides +
+
+ @if (_overrideAttrs.Count == 0) + { +

No overridable (non-locked) attributes in this template.

+ } + else + { + + + + + + + + + + + @foreach (var attr in _overrideAttrs) + { + + + + + + + } + +
AttributeTypeTemplate ValueOverride Value
@attr.Name@attr.DataType@(attr.Value ?? "—") + +
+
+ +
+ } +
+
+ + @* Area Assignment *@ +
+
+ Area Assignment +
+
+
+ + +
+
+
+ } +
+ +@code { + [Parameter] public int Id { get; set; } + + private Instance? _instance; + private string _templateName = ""; + private string _siteName = ""; + private string _areaName = ""; + private bool _loading = true; + private bool _saving; + private string? _errorMessage; + private ToastNotification _toast = default!; + + // Bindings + private List _bindingDataSourceAttrs = new(); + private List _siteConnections = new(); + private Dictionary _bindingSelections = new(); + private int _bulkConnectionId; + + // Overrides + private List _overrideAttrs = new(); + private Dictionary _overrideValues = new(); + + // Area + private List _siteAreas = new(); + private int _reassignAreaId; + + protected override async Task OnInitializedAsync() + { + try + { + _instance = await TemplateEngineRepository.GetInstanceByIdAsync(Id); + if (_instance == null) + { + _errorMessage = $"Instance #{Id} not found."; + _loading = false; + return; + } + + // Identity + var template = await TemplateEngineRepository.GetTemplateByIdAsync(_instance.TemplateId); + _templateName = template?.Name ?? $"#{_instance.TemplateId}"; + + var sites = await SiteRepository.GetAllSitesAsync(); + _siteName = sites.FirstOrDefault(s => s.Id == _instance.SiteId)?.Name ?? $"#{_instance.SiteId}"; + + // Areas + _siteAreas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(_instance.SiteId)).ToList(); + _reassignAreaId = _instance.AreaId ?? 0; + _areaName = _siteAreas.FirstOrDefault(a => a.Id == _reassignAreaId)?.Name ?? ""; + + // Bindings + var attrs = await TemplateEngineRepository.GetAttributesByTemplateIdAsync(_instance.TemplateId); + _bindingDataSourceAttrs = attrs.Where(a => !string.IsNullOrEmpty(a.DataSourceReference)).ToList(); + _siteConnections = (await SiteRepository.GetDataConnectionsBySiteIdAsync(_instance.SiteId)).ToList(); + var existingBindings = await TemplateEngineRepository.GetBindingsByInstanceIdAsync(Id); + foreach (var b in existingBindings) + _bindingSelections[b.AttributeName] = b.DataConnectionId; + + // Overrides + _overrideAttrs = attrs.Where(a => !a.IsLocked).ToList(); + var existingOverrides = await TemplateEngineRepository.GetOverridesByInstanceIdAsync(Id); + foreach (var o in existingOverrides) + _overrideValues[o.AttributeName] = o.OverrideValue; + } + catch (Exception ex) + { + _errorMessage = $"Failed to load instance: {ex.Message}"; + } + _loading = false; + } + + private void GoBack() => NavigationManager.NavigateTo("/deployment/instances"); + + private async Task GetCurrentUserAsync() + { + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + return authState.User.FindFirst("Username")?.Value ?? "unknown"; + } + + // ── Bindings ──────────────────────────────────────────── + + private int GetBindingConnectionId(string attrName) + => _bindingSelections.GetValueOrDefault(attrName, 0); + + private void OnBindingChanged(string attrName, ChangeEventArgs e) + { + var val = int.TryParse(e.Value?.ToString(), out var id) ? id : 0; + if (val == 0) _bindingSelections.Remove(attrName); + else _bindingSelections[attrName] = val; + } + + private void ApplyBulkBinding() + { + if (_bulkConnectionId == 0) return; + foreach (var attr in _bindingDataSourceAttrs) + _bindingSelections[attr.Name] = _bulkConnectionId; + } + + private async Task SaveBindings() + { + _saving = true; + try + { + var bindings = _bindingSelections.Select(kv => (kv.Key, kv.Value)).ToList(); + var user = await GetCurrentUserAsync(); + var result = await InstanceService.SetConnectionBindingsAsync(Id, bindings, user); + if (result.IsSuccess) + _toast.ShowSuccess($"Saved {bindings.Count} connection binding(s)."); + else + _toast.ShowError($"Save failed: {result.Error}"); + } + catch (Exception ex) + { + _toast.ShowError($"Save failed: {ex.Message}"); + } + _saving = false; + } + + // ── Overrides ─────────────────────────────────────────── + + private string? GetOverrideValue(string attrName) + => _overrideValues.GetValueOrDefault(attrName); + + private void OnOverrideChanged(string attrName, ChangeEventArgs e) + { + var val = e.Value?.ToString(); + if (string.IsNullOrEmpty(val)) _overrideValues.Remove(attrName); + else _overrideValues[attrName] = val; + } + + private async Task SaveOverrides() + { + _saving = true; + try + { + var user = await GetCurrentUserAsync(); + foreach (var (attrName, value) in _overrideValues) + await InstanceService.SetAttributeOverrideAsync(Id, attrName, value, user); + _toast.ShowSuccess($"Saved {_overrideValues.Count} override(s)."); + } + catch (Exception ex) + { + _toast.ShowError($"Save overrides failed: {ex.Message}"); + } + _saving = false; + } + + // ── Area ──────────────────────────────────────────────── + + private async Task ReassignArea() + { + _saving = true; + try + { + var user = await GetCurrentUserAsync(); + var result = await InstanceService.AssignToAreaAsync(Id, _reassignAreaId == 0 ? null : _reassignAreaId, user); + if (result.IsSuccess) + { + _areaName = _siteAreas.FirstOrDefault(a => a.Id == _reassignAreaId)?.Name ?? ""; + _toast.ShowSuccess("Area reassigned."); + } + else + _toast.ShowError($"Reassign failed: {result.Error}"); + } + catch (Exception ex) + { + _toast.ShowError($"Reassign failed: {ex.Message}"); + } + _saving = false; + } + + private static string GetStateBadge(InstanceState state) => state switch + { + InstanceState.Enabled => "bg-success", + InstanceState.Disabled => "bg-secondary", + InstanceState.NotDeployed => "bg-light text-dark", + _ => "bg-secondary" + }; +} diff --git a/src/ScadaLink.CentralUI/Components/Pages/Deployment/Instances.razor b/src/ScadaLink.CentralUI/Components/Pages/Deployment/Instances.razor index cd9255a..371087b 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Deployment/Instances.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Deployment/Instances.razor @@ -7,13 +7,11 @@ @using ScadaLink.Commons.Interfaces.Repositories @using ScadaLink.Commons.Types.Enums @using ScadaLink.DeploymentManager -@using ScadaLink.TemplateEngine.Services @attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)] @inject ITemplateEngineRepository TemplateEngineRepository @inject ISiteRepository SiteRepository @inject IDeploymentManagerRepository DeploymentManagerRepository @inject DeploymentService DeploymentService -@inject InstanceService InstanceService @inject AuthenticationStateProvider AuthStateProvider @inject NavigationManager NavigationManager @@ -138,118 +136,13 @@ @onclick="() => EnableInstance(inst)" disabled="@_actionInProgress">Enable } - + @onclick='() => NavigationManager.NavigateTo($"/deployment/instances/{inst.Id}/configure")'>Configure - @if (_overrideInstanceId == inst.Id) - { - - -
- Attribute Overrides for @inst.UniqueName -
- - - -
-
- @if (_overrideAttrs.Count == 0) - { -

No overridable (non-locked) attributes in this template.

- } - else - { - - - - - - @foreach (var attr in _overrideAttrs) - { - - - - - - } - -
AttributeTemplate ValueOverride Value
@attr.Name @attr.DataType@(attr.Value ?? "—") - -
- - } - - - } - @if (_bindingInstanceId == inst.Id) - { - - -
- Connection Bindings for @inst.UniqueName - @if (_bindingDataSourceAttrs.Count > 0 && _siteConnections.Count > 0) - { -
- - -
- } -
- @if (_bindingDataSourceAttrs.Count == 0) - { -

No data-sourced attributes in this template.

- } - else - { - - - - - - @foreach (var attr in _bindingDataSourceAttrs) - { - - - - - - } - -
AttributeTag PathConnection
@attr.Name@attr.DataSourceReference - -
- - } - - - } } @@ -375,7 +268,7 @@ _sites = (await SiteRepository.GetAllSitesAsync()).ToList(); _templates = (await TemplateEngineRepository.GetAllTemplatesAsync()).ToList(); - // Load areas for all sites + // Load areas for display _allAreas.Clear(); foreach (var site in _sites) { @@ -562,85 +455,6 @@ _actionInProgress = false; } - // Override state - private int _overrideInstanceId; - private List _overrideAttrs = new(); - private Dictionary _overrideValues = new(); - private int _reassignAreaId; - - private async Task ToggleOverrides(Instance inst) - { - if (_overrideInstanceId == inst.Id) { _overrideInstanceId = 0; return; } - _overrideInstanceId = inst.Id; - _overrideValues.Clear(); - _reassignAreaId = inst.AreaId ?? 0; - - var attrs = await TemplateEngineRepository.GetAttributesByTemplateIdAsync(inst.TemplateId); - _overrideAttrs = attrs.Where(a => !a.IsLocked).ToList(); - - var overrides = await TemplateEngineRepository.GetOverridesByInstanceIdAsync(inst.Id); - foreach (var o in overrides) - { - _overrideValues[o.AttributeName] = o.OverrideValue; - } - } - - private string? GetOverrideValue(string attrName) => - _overrideValues.GetValueOrDefault(attrName); - - private void OnOverrideChanged(string attrName, ChangeEventArgs e) - { - var val = e.Value?.ToString(); - if (string.IsNullOrEmpty(val)) - _overrideValues.Remove(attrName); - else - _overrideValues[attrName] = val; - } - - private async Task SaveOverrides() - { - _actionInProgress = true; - try - { - var user = await GetCurrentUserAsync(); - foreach (var (attrName, value) in _overrideValues) - { - await InstanceService.SetAttributeOverrideAsync(_overrideInstanceId, attrName, value, user); - } - _toast.ShowSuccess($"Saved {_overrideValues.Count} override(s)."); - _overrideInstanceId = 0; - } - catch (Exception ex) - { - _toast.ShowError($"Save overrides failed: {ex.Message}"); - } - _actionInProgress = false; - } - - private async Task ReassignArea(Instance inst) - { - _actionInProgress = true; - try - { - var user = await GetCurrentUserAsync(); - var result = await InstanceService.AssignToAreaAsync(inst.Id, _reassignAreaId == 0 ? null : _reassignAreaId, user); - if (result.IsSuccess) - { - _toast.ShowSuccess($"Area reassigned for '{inst.UniqueName}'."); - await LoadDataAsync(); - } - else - { - _toast.ShowError($"Reassign failed: {result.Error}"); - } - } - catch (Exception ex) - { - _toast.ShowError($"Reassign failed: {ex.Message}"); - } - _actionInProgress = false; - } - // Diff state private bool _showDiffModal; private bool _diffLoading; @@ -674,95 +488,4 @@ _diffLoading = false; } - // Connection binding state - private int _bindingInstanceId; - private List _bindingDataSourceAttrs = new(); - private List _siteConnections = new(); - private Dictionary _bindingSelections = new(); - private int _bulkConnectionId; - - private async Task ToggleBindings(Instance inst) - { - if (_bindingInstanceId == inst.Id) - { - _bindingInstanceId = 0; - return; - } - - _bindingInstanceId = inst.Id; - _bindingSelections.Clear(); - _bulkConnectionId = 0; - - // Load template attributes with DataSourceReference - var attrs = await TemplateEngineRepository.GetAttributesByTemplateIdAsync(inst.TemplateId); - _bindingDataSourceAttrs = attrs.Where(a => !string.IsNullOrEmpty(a.DataSourceReference)).ToList(); - - // Load data connections for this site (each connection now belongs to exactly one site) - _siteConnections = (await SiteRepository.GetDataConnectionsBySiteIdAsync(inst.SiteId)).ToList(); - - // Load existing bindings - var existingBindings = await TemplateEngineRepository.GetBindingsByInstanceIdAsync(inst.Id); - foreach (var b in existingBindings) - { - _bindingSelections[b.AttributeName] = b.DataConnectionId; - } - } - - private int GetBindingConnectionId(string attrName) - { - return _bindingSelections.GetValueOrDefault(attrName, 0); - } - - private void OnBindingChanged(string attrName, ChangeEventArgs e) - { - var val = int.TryParse(e.Value?.ToString(), out var id) ? id : 0; - SetBinding(attrName, val); - } - - private void SetBinding(string attrName, int connectionId) - { - if (connectionId == 0) - _bindingSelections.Remove(attrName); - else - _bindingSelections[attrName] = connectionId; - } - - private void ApplyBulkBinding() - { - if (_bulkConnectionId == 0) return; - foreach (var attr in _bindingDataSourceAttrs) - { - _bindingSelections[attr.Name] = _bulkConnectionId; - } - } - - private async Task SaveBindings() - { - _actionInProgress = true; - try - { - var bindings = _bindingSelections - .Select(kv => (kv.Key, kv.Value)) - .ToList(); - - var user = await GetCurrentUserAsync(); - var result = await InstanceService.SetConnectionBindingsAsync( - _bindingInstanceId, bindings, user); - - if (result.IsSuccess) - { - _toast.ShowSuccess($"Saved {bindings.Count} connection bindings."); - _bindingInstanceId = 0; - } - else - { - _toast.ShowError($"Save failed: {result.Error}"); - } - } - catch (Exception ex) - { - _toast.ShowError($"Save failed: {ex.Message}"); - } - _actionInProgress = false; - } }