refactor: simplify data connections from many-to-many site assignment to direct site ownership

Replace SiteDataConnectionAssignment join table with a direct SiteId FK on DataConnection,
simplifying the data model, repositories, UI, CLI, and deployment service.
This commit is contained in:
Joseph Doherty
2026-03-21 21:07:10 -04:00
parent cd6efeea90
commit 970d0a5cb3
25 changed files with 1543 additions and 490 deletions

View File

@@ -21,6 +21,26 @@
{
<div class="card mb-3">
<div class="card-body">
@if (Id.HasValue)
{
<div class="mb-2">
<label class="form-label small">Site</label>
<input type="text" class="form-control form-control-sm" value="@(_siteName)" disabled />
</div>
}
else
{
<div class="mb-2">
<label class="form-label small">Site</label>
<select class="form-select form-select-sm" @bind="_formSiteId">
<option value="0">Select site...</option>
@foreach (var site in _sites)
{
<option value="@site.Id">@site.Name</option>
}
</select>
</div>
}
<div class="mb-2">
<label class="form-label small">Name</label>
<input type="text" class="form-control form-control-sm" @bind="_formName" />
@@ -57,6 +77,9 @@
private bool _loading = true;
private DataConnection? _editingConnection;
private List<Site> _sites = new();
private int _formSiteId;
private string _siteName = string.Empty;
private string _formName = string.Empty;
private string _formProtocol = string.Empty;
private string? _formConfiguration;
@@ -64,6 +87,8 @@
protected override async Task OnInitializedAsync()
{
_sites = (await SiteRepository.GetAllSitesAsync()).ToList();
if (Id.HasValue)
{
try
@@ -71,6 +96,8 @@
_editingConnection = await SiteRepository.GetDataConnectionByIdAsync(Id.Value);
if (_editingConnection != null)
{
_formSiteId = _editingConnection.SiteId;
_siteName = _sites.FirstOrDefault(s => s.Id == _formSiteId)?.Name ?? $"Site {_formSiteId}";
_formName = _editingConnection.Name;
_formProtocol = _editingConnection.Protocol;
_formConfiguration = _editingConnection.Configuration;
@@ -87,6 +114,7 @@
private async Task SaveConnection()
{
_formError = null;
if (_formSiteId == 0) { _formError = "Site is required."; return; }
if (string.IsNullOrWhiteSpace(_formName)) { _formError = "Name is required."; return; }
if (string.IsNullOrWhiteSpace(_formProtocol)) { _formError = "Protocol is required."; return; }
@@ -101,7 +129,7 @@
}
else
{
var conn = new DataConnection(_formName.Trim(), _formProtocol)
var conn = new DataConnection(_formName.Trim(), _formProtocol, _formSiteId)
{
Configuration = _formConfiguration?.Trim()
};

View File

@@ -25,58 +25,14 @@
}
else
{
@* Assignment form *@
@if (_showAssignForm)
{
<div class="card mb-3">
<div class="card-body">
<h6 class="card-title">Assign Connection to Site</h6>
<div class="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label small">Connection</label>
<select class="form-select form-select-sm" @bind="_assignConnectionId">
<option value="0">Select connection...</option>
@foreach (var conn in _connections)
{
<option value="@conn.Id">@conn.Name (@conn.Protocol)</option>
}
</select>
</div>
<div class="col-md-3">
<label class="form-label small">Site</label>
<select class="form-select form-select-sm" @bind="_assignSiteId">
<option value="0">Select site...</option>
@foreach (var site in _sites)
{
<option value="@site.Id">@site.Name</option>
}
</select>
</div>
<div class="col-md-3">
<button class="btn btn-success btn-sm me-1" @onclick="SaveAssignment">Assign</button>
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelAssignForm">Cancel</button>
</div>
</div>
@if (_assignError != null)
{
<div class="text-danger small mt-1">@_assignError</div>
}
</div>
</div>
}
<div class="mb-2">
<button class="btn btn-outline-info btn-sm" @onclick="ShowAssignForm">Assign to Site</button>
</div>
<table class="table table-sm table-striped table-hover">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>Name</th>
<th>Protocol</th>
<th>Site</th>
<th>Configuration</th>
<th>Assigned Sites</th>
<th style="width: 160px;">Actions</th>
</tr>
</thead>
@@ -93,29 +49,8 @@
<td>@conn.Id</td>
<td>@conn.Name</td>
<td><span class="badge bg-secondary">@conn.Protocol</span></td>
<td>@(_siteLookup.GetValueOrDefault(conn.SiteId)?.Name ?? $"Site {conn.SiteId}")</td>
<td class="text-muted small text-truncate" style="max-width: 300px;">@(conn.Configuration ?? "—")</td>
<td>
@{
var assignedSites = _connectionSites.GetValueOrDefault(conn.Id);
}
@if (assignedSites != null && assignedSites.Count > 0)
{
@foreach (var assignment in assignedSites)
{
var siteName = _sites.FirstOrDefault(s => s.Id == assignment.SiteId)?.Name ?? $"Site {assignment.SiteId}";
<span class="badge bg-info text-dark me-1">
@siteName
<button type="button" class="btn-close btn-close-white ms-1"
style="font-size: 0.5rem;"
@onclick="() => RemoveAssignment(assignment)"></button>
</span>
}
}
else
{
<span class="text-muted small">None</span>
}
</td>
<td>
<button class="btn btn-outline-primary btn-sm py-0 px-1 me-1"
@onclick='() => NavigationManager.NavigateTo($"/admin/data-connections/{conn.Id}/edit")'>Edit</button>
@@ -131,16 +66,10 @@
@code {
private List<DataConnection> _connections = new();
private List<Site> _sites = new();
private Dictionary<int, List<SiteDataConnectionAssignment>> _connectionSites = new();
private Dictionary<int, Site> _siteLookup = new();
private bool _loading = true;
private string? _errorMessage;
private bool _showAssignForm;
private int _assignConnectionId;
private int _assignSiteId;
private string? _assignError;
private ToastNotification _toast = default!;
private ConfirmDialog _confirmDialog = default!;
@@ -155,24 +84,9 @@
_errorMessage = null;
try
{
_sites = (await SiteRepository.GetAllSitesAsync()).ToList();
var sites = await SiteRepository.GetAllSitesAsync();
_siteLookup = sites.ToDictionary(s => s.Id);
_connections = (await SiteRepository.GetAllDataConnectionsAsync()).ToList();
// Load site assignments for each connection
_connectionSites.Clear();
foreach (var site in _sites)
{
var siteConns = await SiteRepository.GetDataConnectionsBySiteIdAsync(site.Id);
foreach (var conn in siteConns)
{
if (!_connectionSites.ContainsKey(conn.Id))
_connectionSites[conn.Id] = new List<SiteDataConnectionAssignment>();
var assignment = await SiteRepository.GetSiteDataConnectionAssignmentAsync(site.Id, conn.Id);
if (assignment != null)
_connectionSites[conn.Id].Add(assignment);
}
}
}
catch (Exception ex)
{
@@ -199,58 +113,4 @@
_toast.ShowError($"Delete failed: {ex.Message}");
}
}
private void ShowAssignForm()
{
_assignConnectionId = 0;
_assignSiteId = 0;
_assignError = null;
_showAssignForm = true;
}
private void CancelAssignForm()
{
_showAssignForm = false;
_assignError = null;
}
private async Task SaveAssignment()
{
_assignError = null;
if (_assignConnectionId == 0) { _assignError = "Select a connection."; return; }
if (_assignSiteId == 0) { _assignError = "Select a site."; return; }
try
{
var assignment = new SiteDataConnectionAssignment
{
SiteId = _assignSiteId,
DataConnectionId = _assignConnectionId
};
await SiteRepository.AddSiteDataConnectionAssignmentAsync(assignment);
await SiteRepository.SaveChangesAsync();
_showAssignForm = false;
_toast.ShowSuccess("Connection assigned to site.");
await LoadDataAsync();
}
catch (Exception ex)
{
_assignError = $"Assignment failed: {ex.Message}";
}
}
private async Task RemoveAssignment(SiteDataConnectionAssignment assignment)
{
try
{
await SiteRepository.DeleteSiteDataConnectionAssignmentAsync(assignment.Id);
await SiteRepository.SaveChangesAsync();
_toast.ShowSuccess("Assignment removed.");
await LoadDataAsync();
}
catch (Exception ex)
{
_toast.ShowError($"Remove failed: {ex.Message}");
}
}
}

View File

@@ -178,10 +178,9 @@
_deploying = true;
try
{
var command = await ArtifactDeploymentService.BuildDeployArtifactsCommandAsync();
var user = await GetCurrentUserAsync();
var result = await ArtifactDeploymentService.RetryForSiteAsync(
site.SiteIdentifier, command, user);
site.Id, site.SiteIdentifier, user);
if (result.IsSuccess)
_toast.ShowSuccess($"Artifacts deployed to '{site.Name}'.");
@@ -203,9 +202,8 @@
_deploying = true;
try
{
var command = await ArtifactDeploymentService.BuildDeployArtifactsCommandAsync();
var user = await GetCurrentUserAsync();
var result = await ArtifactDeploymentService.DeployToAllSitesAsync(command, user);
var result = await ArtifactDeploymentService.DeployToAllSitesAsync(user);
if (result.IsSuccess)
{

View File

@@ -697,13 +697,8 @@
var attrs = await TemplateEngineRepository.GetAttributesByTemplateIdAsync(inst.TemplateId);
_bindingDataSourceAttrs = attrs.Where(a => !string.IsNullOrEmpty(a.DataSourceReference)).ToList();
// Load data connections for this site
// Load data connections for this site (each connection now belongs to exactly one site)
_siteConnections = (await SiteRepository.GetDataConnectionsBySiteIdAsync(inst.SiteId)).ToList();
if (_siteConnections.Count == 0)
{
// Also show unassigned connections (they may not be assigned to a site yet)
_siteConnections = (await SiteRepository.GetAllDataConnectionsAsync()).ToList();
}
// Load existing bindings
var existingBindings = await TemplateEngineRepository.GetBindingsByInstanceIdAsync(inst.Id);