feat(centralui): protocol selector + MxGateway editor in DataConnectionForm
Adds an OPC UA | MxGateway protocol dropdown (create-time; locked read-only on edit), branches the primary/backup endpoint editors, serializer, and validator by protocol, and persists DataConnection.Protocol accordingly. Updates form tests: protocol dropdown present on create + MxGateway save round-trips typed JSON with Protocol=MxGateway.
This commit is contained in:
+116
-32
@@ -49,17 +49,45 @@
|
|||||||
</select>
|
</select>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label small">Protocol</label>
|
||||||
|
@if (_protocolLocked)
|
||||||
|
{
|
||||||
|
<input type="text"
|
||||||
|
class="form-control form-control-plaintext form-control-sm"
|
||||||
|
readonly
|
||||||
|
value="@_protocol" />
|
||||||
|
<div class="form-text">Protocol is locked after creation.</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<select class="form-select form-select-sm" @bind="_protocol">
|
||||||
|
<option value="OpcUa">OPC UA</option>
|
||||||
|
<option value="MxGateway">MxGateway</option>
|
||||||
|
</select>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label small">Name</label>
|
<label class="form-label small">Name</label>
|
||||||
<input type="text" class="form-control form-control-sm" @bind="_formName" />
|
<input type="text" class="form-control form-control-sm" @bind="_formName" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h6 class="text-muted mt-3">Primary endpoint</h6>
|
<h6 class="text-muted mt-3">Primary endpoint</h6>
|
||||||
<OpcUaEndpointEditor Title="Primary Endpoint"
|
@if (_protocol == "MxGateway")
|
||||||
IdPrefix="primary"
|
{
|
||||||
Config="_primaryConfig"
|
<MxGatewayEndpointEditor Title="Primary Endpoint"
|
||||||
IsLegacy="_primaryIsLegacy"
|
IdPrefix="primary"
|
||||||
Errors="_primaryErrors" />
|
Config="_primaryMx"
|
||||||
|
Errors="_primaryErrors" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<OpcUaEndpointEditor Title="Primary Endpoint"
|
||||||
|
IdPrefix="primary"
|
||||||
|
Config="_primaryConfig"
|
||||||
|
IsLegacy="_primaryIsLegacy"
|
||||||
|
Errors="_primaryErrors" />
|
||||||
|
}
|
||||||
|
|
||||||
<h6 class="text-muted mt-3">
|
<h6 class="text-muted mt-3">
|
||||||
Backup endpoint
|
Backup endpoint
|
||||||
@@ -77,11 +105,21 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<OpcUaEndpointEditor Title="Backup Endpoint"
|
@if (_protocol == "MxGateway")
|
||||||
IdPrefix="backup"
|
{
|
||||||
Config="_backupConfig"
|
<MxGatewayEndpointEditor Title="Backup Endpoint"
|
||||||
IsLegacy="_backupIsLegacy"
|
IdPrefix="backup"
|
||||||
Errors="_backupErrors" />
|
Config="_backupMx"
|
||||||
|
Errors="_backupErrors" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<OpcUaEndpointEditor Title="Backup Endpoint"
|
||||||
|
IdPrefix="backup"
|
||||||
|
Config="_backupConfig"
|
||||||
|
IsLegacy="_backupIsLegacy"
|
||||||
|
Errors="_backupErrors" />
|
||||||
|
}
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label small">Failover Retry Count</label>
|
<label class="form-label small">Failover Retry Count</label>
|
||||||
<input type="number" class="form-control form-control-sm" style="max-width: 120px;"
|
<input type="number" class="form-control form-control-sm" style="max-width: 120px;"
|
||||||
@@ -118,8 +156,12 @@
|
|||||||
private string _siteName = string.Empty;
|
private string _siteName = string.Empty;
|
||||||
private bool _siteLocked;
|
private bool _siteLocked;
|
||||||
private string _formName = string.Empty;
|
private string _formName = string.Empty;
|
||||||
|
private string _protocol = "OpcUa";
|
||||||
|
private bool _protocolLocked;
|
||||||
private OpcUaEndpointConfig _primaryConfig = new();
|
private OpcUaEndpointConfig _primaryConfig = new();
|
||||||
private OpcUaEndpointConfig _backupConfig = new();
|
private OpcUaEndpointConfig _backupConfig = new();
|
||||||
|
private MxGatewayEndpointConfig _primaryMx = new();
|
||||||
|
private MxGatewayEndpointConfig _backupMx = new();
|
||||||
private bool _primaryIsLegacy;
|
private bool _primaryIsLegacy;
|
||||||
private bool _backupIsLegacy;
|
private bool _backupIsLegacy;
|
||||||
private bool _showBackup;
|
private bool _showBackup;
|
||||||
@@ -143,17 +185,10 @@
|
|||||||
_siteName = _sites.FirstOrDefault(s => s.Id == _formSiteId)?.Name ?? $"Site {_formSiteId}";
|
_siteName = _sites.FirstOrDefault(s => s.Id == _formSiteId)?.Name ?? $"Site {_formSiteId}";
|
||||||
_siteLocked = true;
|
_siteLocked = true;
|
||||||
_formName = _editingConnection.Name;
|
_formName = _editingConnection.Name;
|
||||||
|
_protocol = string.IsNullOrWhiteSpace(_editingConnection.Protocol) ? "OpcUa" : _editingConnection.Protocol;
|
||||||
|
_protocolLocked = true;
|
||||||
|
|
||||||
(_primaryConfig, _primaryIsLegacy) =
|
LoadConfig(_editingConnection);
|
||||||
OpcUaEndpointConfigSerializer.Deserialize(_editingConnection.PrimaryConfiguration);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_editingConnection.BackupConfiguration))
|
|
||||||
{
|
|
||||||
(_backupConfig, _backupIsLegacy) =
|
|
||||||
OpcUaEndpointConfigSerializer.Deserialize(_editingConnection.BackupConfiguration);
|
|
||||||
_showBackup = true;
|
|
||||||
_formFailoverRetryCount = _editingConnection.FailoverRetryCount;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (SiteId.HasValue)
|
else if (SiteId.HasValue)
|
||||||
@@ -177,32 +212,80 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LoadConfig(DataConnection conn)
|
||||||
|
{
|
||||||
|
if (_protocol == "MxGateway")
|
||||||
|
{
|
||||||
|
_primaryMx = MxGatewayEndpointConfigSerializer.Deserialize(conn.PrimaryConfiguration);
|
||||||
|
if (!string.IsNullOrWhiteSpace(conn.BackupConfiguration))
|
||||||
|
{
|
||||||
|
_backupMx = MxGatewayEndpointConfigSerializer.Deserialize(conn.BackupConfiguration);
|
||||||
|
_showBackup = true;
|
||||||
|
_formFailoverRetryCount = conn.FailoverRetryCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
(_primaryConfig, _primaryIsLegacy) =
|
||||||
|
OpcUaEndpointConfigSerializer.Deserialize(conn.PrimaryConfiguration);
|
||||||
|
if (!string.IsNullOrWhiteSpace(conn.BackupConfiguration))
|
||||||
|
{
|
||||||
|
(_backupConfig, _backupIsLegacy) =
|
||||||
|
OpcUaEndpointConfigSerializer.Deserialize(conn.BackupConfiguration);
|
||||||
|
_showBackup = true;
|
||||||
|
_formFailoverRetryCount = conn.FailoverRetryCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SaveConnection()
|
private async Task SaveConnection()
|
||||||
{
|
{
|
||||||
_formError = null;
|
_formError = null;
|
||||||
if (_formSiteId == 0) { _formError = "Site is required."; return; }
|
if (_formSiteId == 0) { _formError = "Site is required."; return; }
|
||||||
if (string.IsNullOrWhiteSpace(_formName)) { _formError = "Name is required."; return; }
|
if (string.IsNullOrWhiteSpace(_formName)) { _formError = "Name is required."; return; }
|
||||||
|
|
||||||
_primaryErrors = OpcUaEndpointConfigValidator.Validate(_primaryConfig, "Primary.");
|
string primaryJson;
|
||||||
_backupErrors = _showBackup
|
string? backupJson;
|
||||||
? OpcUaEndpointConfigValidator.Validate(_backupConfig, "Backup.")
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (!_primaryErrors.IsValid || (_backupErrors is { IsValid: false }))
|
if (_protocol == "MxGateway")
|
||||||
{
|
{
|
||||||
_formError = "Fix the errors below before saving.";
|
_primaryErrors = MxGatewayEndpointConfigValidator.Validate(_primaryMx, "Primary.");
|
||||||
return;
|
_backupErrors = _showBackup
|
||||||
}
|
? MxGatewayEndpointConfigValidator.Validate(_backupMx, "Backup.")
|
||||||
|
: null;
|
||||||
|
|
||||||
var primaryJson = OpcUaEndpointConfigSerializer.Serialize(_primaryConfig);
|
if (!_primaryErrors.IsValid || (_backupErrors is { IsValid: false }))
|
||||||
var backupJson = _showBackup ? OpcUaEndpointConfigSerializer.Serialize(_backupConfig) : null;
|
{
|
||||||
|
_formError = "Fix the errors below before saving.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryJson = MxGatewayEndpointConfigSerializer.Serialize(_primaryMx);
|
||||||
|
backupJson = _showBackup ? MxGatewayEndpointConfigSerializer.Serialize(_backupMx) : null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_primaryErrors = OpcUaEndpointConfigValidator.Validate(_primaryConfig, "Primary.");
|
||||||
|
_backupErrors = _showBackup
|
||||||
|
? OpcUaEndpointConfigValidator.Validate(_backupConfig, "Backup.")
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!_primaryErrors.IsValid || (_backupErrors is { IsValid: false }))
|
||||||
|
{
|
||||||
|
_formError = "Fix the errors below before saving.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryJson = OpcUaEndpointConfigSerializer.Serialize(_primaryConfig);
|
||||||
|
backupJson = _showBackup ? OpcUaEndpointConfigSerializer.Serialize(_backupConfig) : null;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_editingConnection != null)
|
if (_editingConnection != null)
|
||||||
{
|
{
|
||||||
_editingConnection.Name = _formName.Trim();
|
_editingConnection.Name = _formName.Trim();
|
||||||
_editingConnection.Protocol = "OpcUa";
|
_editingConnection.Protocol = _protocol;
|
||||||
_editingConnection.PrimaryConfiguration = primaryJson;
|
_editingConnection.PrimaryConfiguration = primaryJson;
|
||||||
_editingConnection.BackupConfiguration = backupJson;
|
_editingConnection.BackupConfiguration = backupJson;
|
||||||
_editingConnection.FailoverRetryCount = _showBackup ? _formFailoverRetryCount : 3;
|
_editingConnection.FailoverRetryCount = _showBackup ? _formFailoverRetryCount : 3;
|
||||||
@@ -210,7 +293,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var conn = new DataConnection(_formName.Trim(), "OpcUa", _formSiteId)
|
var conn = new DataConnection(_formName.Trim(), _protocol, _formSiteId)
|
||||||
{
|
{
|
||||||
PrimaryConfiguration = primaryJson,
|
PrimaryConfiguration = primaryJson,
|
||||||
BackupConfiguration = backupJson,
|
BackupConfiguration = backupJson,
|
||||||
@@ -233,6 +316,7 @@
|
|||||||
{
|
{
|
||||||
_showBackup = false;
|
_showBackup = false;
|
||||||
_backupConfig = new OpcUaEndpointConfig();
|
_backupConfig = new OpcUaEndpointConfig();
|
||||||
|
_backupMx = new MxGatewayEndpointConfig();
|
||||||
_backupIsLegacy = false;
|
_backupIsLegacy = false;
|
||||||
_formFailoverRetryCount = 3;
|
_formFailoverRetryCount = 3;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,12 +44,55 @@ public class DataConnectionFormTests : BunitContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NoProtocolDropdown_IsRendered()
|
public void ProtocolDropdown_IsRendered_OnCreate_WithBothProtocols()
|
||||||
{
|
{
|
||||||
var cut = RenderForCreateSite(1);
|
var cut = RenderForCreateSite(1);
|
||||||
Assert.DoesNotContain("Custom", cut.Markup);
|
|
||||||
var labels = cut.FindAll("label").Select(l => l.TextContent.Trim()).ToList();
|
var labels = cut.FindAll("label").Select(l => l.TextContent.Trim()).ToList();
|
||||||
Assert.DoesNotContain(labels, l => l == "Protocol");
|
Assert.Contains(labels, l => l == "Protocol");
|
||||||
|
|
||||||
|
// The protocol select offers OPC UA and MxGateway.
|
||||||
|
var optionTexts = cut.FindAll("option").Select(o => o.TextContent.Trim()).ToList();
|
||||||
|
Assert.Contains("OPC UA", optionTexts);
|
||||||
|
Assert.Contains("MxGateway", optionTexts);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Save_MxGateway_PersistsTypedJsonAndProtocolMxGateway()
|
||||||
|
{
|
||||||
|
DataConnection? captured = null;
|
||||||
|
await _siteRepo.AddDataConnectionAsync(
|
||||||
|
Arg.Do<DataConnection>(d => captured = d));
|
||||||
|
|
||||||
|
var cut = RenderForCreateSite(1);
|
||||||
|
|
||||||
|
// Switch protocol to MxGateway — re-renders with the MxGateway editor.
|
||||||
|
cut.FindAll("select")
|
||||||
|
.First(s => s.QuerySelectorAll("option").Any(o => o.TextContent.Trim() == "MxGateway"))
|
||||||
|
.Change("MxGateway");
|
||||||
|
|
||||||
|
// Name (skip readonly Site plaintext input; MxGateway editor inputs carry placeholders).
|
||||||
|
cut.FindAll("input[type='text']")
|
||||||
|
.First(i => !i.HasAttribute("readonly") && i.GetAttribute("placeholder") is null)
|
||||||
|
.Change("MX-1");
|
||||||
|
// Gateway endpoint
|
||||||
|
cut.FindAll("input[type='text']")
|
||||||
|
.First(i => i.GetAttribute("placeholder")?.StartsWith("http://") == true)
|
||||||
|
.Change("http://gw:5000");
|
||||||
|
// API key (password input)
|
||||||
|
cut.FindAll("input[type='password']")
|
||||||
|
.First(i => i.GetAttribute("placeholder")?.Contains("API key") == true)
|
||||||
|
.Change("secret");
|
||||||
|
|
||||||
|
await cut.FindAll("button")
|
||||||
|
.First(b => b.TextContent.Trim() == "Save").ClickAsync(new());
|
||||||
|
|
||||||
|
Assert.NotNull(captured);
|
||||||
|
Assert.Equal("MxGateway", captured!.Protocol);
|
||||||
|
Assert.NotNull(captured.PrimaryConfiguration);
|
||||||
|
|
||||||
|
using var doc = JsonDocument.Parse(captured.PrimaryConfiguration!);
|
||||||
|
Assert.Equal("http://gw:5000",
|
||||||
|
doc.RootElement.GetProperty("endpoint").GetString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user