9e243493fb
Applies the family-wide admin-UI cleanup playbook to the Central UI so the Blazor surfaces stop diverging from the shared kit: buttons are grouped rather than individually sized, long cell values are contained instead of widening tables, and hard-coded colours give way to theme tokens. The headline fix is that MainLayout passed Accent="#2f5fd0" to ThemeShell, which the kit emits as an inline style on the shell root. Being a descendant of <html>, it beat the [data-bs-theme="dark"] override for the entire app, so the dark accent had never rendered. Declaring --accent in site.css :root instead lets both schemes resolve; light is unchanged because the value already matched the kit's light default. Theme pins to 0.4.1, which upstreams the local .btn sizing block verbatim, so that block is deleted here rather than duplicated. Verified byte-identical before removal; the repo now declares no --bs-btn-* anywhere. NOT purely cosmetic, contrary to the sweep's stated scope: four detail-modal surfaces (NotificationReport, ConfigurationAuditLog, ParkedMessages, SiteCallsReport) were additionally refactored from holding the selected record to holding its id and re-resolving from the current page each render, with the resolve doubling as the visibility gate. A background refresh that drops the row now closes the modal instead of showing a stale snapshot. This is a behaviour change and is called out rather than buried: a full-suite run turned up one intermittent CentralUI failure, CloseButton_DismissesModal, whose stack (GetRequiredEventBindingEntry during DispatchEventAsync) indicates the handler was disposed between render and click — a window the previous field-held record made structurally impossible. Treat the modal lifecycle here as unreviewed. Build 0/0; suite green apart from that one intermittent failure.
371 lines
15 KiB
Plaintext
371 lines
15 KiB
Plaintext
@page "/design/connections/create"
|
|
@page "/design/connections/{Id:int}/edit"
|
|
@page "/design/data-connections/create"
|
|
@page "/design/data-connections/{Id:int}/edit"
|
|
@using ZB.MOM.WW.ScadaBridge.Security
|
|
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites
|
|
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
|
@using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections
|
|
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening
|
|
@using ZB.MOM.WW.ScadaBridge.Commons.Serialization
|
|
@using ZB.MOM.WW.ScadaBridge.Commons.Validators
|
|
@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Forms
|
|
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
|
|
@inject ISiteRepository SiteRepository
|
|
@inject NavigationManager NavigationManager
|
|
|
|
<div class="container-fluid mt-3">
|
|
<div class="d-flex align-items-center mb-3">
|
|
<button class="btn btn-outline-secondary btn-sm me-3" @onclick="GoBack">← Back</button>
|
|
<h4 class="mb-0">@(Id.HasValue ? "Edit Data Connection" : "Add Data Connection")</h4>
|
|
</div>
|
|
|
|
@if (_loading)
|
|
{
|
|
<LoadingSpinner IsLoading="true" />
|
|
}
|
|
else
|
|
{
|
|
<div class="card mb-3">
|
|
<div class="card-header">
|
|
<span class="fw-semibold">Connection</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-2">
|
|
<label class="form-label small">Site</label>
|
|
@if (_siteLocked)
|
|
{
|
|
<input type="text"
|
|
class="form-control form-control-plaintext form-control-sm"
|
|
readonly
|
|
value="@_siteName" />
|
|
<div class="form-text">Site is locked after creation.</div>
|
|
}
|
|
else
|
|
{
|
|
<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">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">
|
|
<label class="form-label small">Name</label>
|
|
<input type="text" class="form-control form-control-sm" @bind="_formName" />
|
|
</div>
|
|
|
|
@if (Id.HasValue && _protocol == "OpcUa")
|
|
{
|
|
<div class="mb-2">
|
|
<a class="btn btn-outline-secondary btn-sm"
|
|
data-test="manage-certificates-link"
|
|
href="@($"/design/connections/{Id}/certificates")">
|
|
Manage certificates
|
|
</a>
|
|
<div class="form-text">
|
|
View, trust, and remove OPC UA server certificates in the
|
|
site's node-wide trusted-peer store.
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@* Each endpoint editor is a heavyweight sub-form, so it gets its own card
|
|
rather than sharing one body separated only by a muted <h6>. *@
|
|
<div class="card mb-3">
|
|
<div class="card-header">
|
|
<span class="fw-semibold">Primary endpoint</span>
|
|
</div>
|
|
<div class="card-body">
|
|
@if (_protocol == "MxGateway")
|
|
{
|
|
<MxGatewayEndpointEditor Title="Primary Endpoint"
|
|
IdPrefix="primary"
|
|
Config="_primaryMx"
|
|
Errors="_primaryErrors" />
|
|
}
|
|
else
|
|
{
|
|
<OpcUaEndpointEditor Title="Primary Endpoint"
|
|
IdPrefix="primary"
|
|
Config="_primaryConfig"
|
|
IsLegacy="_primaryIsLegacy"
|
|
SiteIdentifier="@_formSiteIdentifier"
|
|
ConnectionName="@_formName"
|
|
Protocol="@_protocol"
|
|
Errors="_primaryErrors" />
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-3">
|
|
<div class="card-header d-flex align-items-center">
|
|
<span class="fw-semibold">Backup endpoint</span>
|
|
@if (!_showBackup)
|
|
{
|
|
<span class="badge bg-body-secondary text-body-secondary border ms-2">Optional</span>
|
|
}
|
|
</div>
|
|
<div class="card-body">
|
|
@if (!_showBackup)
|
|
{
|
|
<div class="mb-0">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
|
@onclick="EnableBackup">Add Backup Endpoint</button>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
@if (_protocol == "MxGateway")
|
|
{
|
|
<MxGatewayEndpointEditor Title="Backup Endpoint"
|
|
IdPrefix="backup"
|
|
Config="_backupMx"
|
|
Errors="_backupErrors" />
|
|
}
|
|
else
|
|
{
|
|
<OpcUaEndpointEditor Title="Backup Endpoint"
|
|
IdPrefix="backup"
|
|
Config="_backupConfig"
|
|
IsLegacy="_backupIsLegacy"
|
|
SiteIdentifier="@_formSiteIdentifier"
|
|
ConnectionName="@_formName"
|
|
Protocol="@_protocol"
|
|
Errors="_backupErrors" />
|
|
}
|
|
<div class="mb-2">
|
|
<label class="form-label small">Failover Retry Count</label>
|
|
<input type="number" class="form-control form-control-sm" style="max-width: 120px;"
|
|
min="1" max="20" @bind="_formFailoverRetryCount" />
|
|
<div class="form-text">Retries before failing over to backup endpoint.</div>
|
|
</div>
|
|
<div class="mb-0">
|
|
<button type="button" class="btn btn-outline-danger btn-sm"
|
|
@onclick="RemoveBackup">Remove Backup</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@if (_formError != null)
|
|
{
|
|
<div class="text-danger small mt-2">@_formError</div>
|
|
}
|
|
<div class="mb-3">
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
<button class="btn btn-success" @onclick="SaveConnection">Save</button>
|
|
<button class="btn btn-outline-secondary" @onclick="GoBack">Cancel</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@code {
|
|
[Parameter] public int? Id { get; set; }
|
|
[SupplyParameterFromQuery] public int? SiteId { get; set; }
|
|
|
|
private bool _loading = true;
|
|
private DataConnection? _editingConnection;
|
|
private List<Site> _sites = new();
|
|
private int _formSiteId;
|
|
private string _siteName = string.Empty;
|
|
private bool _siteLocked;
|
|
private string _formName = string.Empty;
|
|
private string _protocol = "OpcUa";
|
|
private bool _protocolLocked;
|
|
private OpcUaEndpointConfig _primaryConfig = new();
|
|
private OpcUaEndpointConfig _backupConfig = new();
|
|
private MxGatewayEndpointConfig _primaryMx = new();
|
|
private MxGatewayEndpointConfig _backupMx = new();
|
|
private bool _primaryIsLegacy;
|
|
private bool _backupIsLegacy;
|
|
private bool _showBackup;
|
|
private int _formFailoverRetryCount = 3;
|
|
private ValidationResult? _primaryErrors;
|
|
private ValidationResult? _backupErrors;
|
|
private string? _formError;
|
|
|
|
// The machine-readable site identifier (used in Akka addresses) for the currently
|
|
// selected site — resolved from _formSiteId (the numeric primary key) so the
|
|
// endpoint editor's Verify probe can target the owning site. Blank until a site is
|
|
// chosen, which disables verification in the editor.
|
|
private string _formSiteIdentifier =>
|
|
_sites.FirstOrDefault(s => s.Id == _formSiteId)?.SiteIdentifier ?? string.Empty;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
try
|
|
{
|
|
_sites = (await SiteRepository.GetAllSitesAsync()).ToList();
|
|
|
|
if (Id.HasValue)
|
|
{
|
|
_editingConnection = await SiteRepository.GetDataConnectionByIdAsync(Id.Value);
|
|
if (_editingConnection != null)
|
|
{
|
|
_formSiteId = _editingConnection.SiteId;
|
|
_siteName = _sites.FirstOrDefault(s => s.Id == _formSiteId)?.Name ?? $"Site {_formSiteId}";
|
|
_siteLocked = true;
|
|
_formName = _editingConnection.Name;
|
|
_protocol = string.IsNullOrWhiteSpace(_editingConnection.Protocol) ? "OpcUa" : _editingConnection.Protocol;
|
|
_protocolLocked = true;
|
|
|
|
LoadConfig(_editingConnection);
|
|
}
|
|
}
|
|
else if (SiteId.HasValue)
|
|
{
|
|
var site = _sites.FirstOrDefault(s => s.Id == SiteId.Value);
|
|
if (site != null)
|
|
{
|
|
_formSiteId = site.Id;
|
|
_siteName = site.Name;
|
|
_siteLocked = true;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_formError = $"Failed to load: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
_formError = null;
|
|
if (_formSiteId == 0) { _formError = "Site is required."; return; }
|
|
if (string.IsNullOrWhiteSpace(_formName)) { _formError = "Name is required."; return; }
|
|
|
|
string primaryJson;
|
|
string? backupJson;
|
|
|
|
if (_protocol == "MxGateway")
|
|
{
|
|
_primaryErrors = MxGatewayEndpointConfigValidator.Validate(_primaryMx, "Primary.");
|
|
_backupErrors = _showBackup
|
|
? MxGatewayEndpointConfigValidator.Validate(_backupMx, "Backup.")
|
|
: null;
|
|
|
|
if (!_primaryErrors.IsValid || (_backupErrors is { IsValid: false }))
|
|
{
|
|
_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
|
|
{
|
|
if (_editingConnection != null)
|
|
{
|
|
_editingConnection.Name = _formName.Trim();
|
|
_editingConnection.Protocol = _protocol;
|
|
_editingConnection.PrimaryConfiguration = primaryJson;
|
|
_editingConnection.BackupConfiguration = backupJson;
|
|
_editingConnection.FailoverRetryCount = _showBackup ? _formFailoverRetryCount : 3;
|
|
await SiteRepository.UpdateDataConnectionAsync(_editingConnection);
|
|
}
|
|
else
|
|
{
|
|
var conn = new DataConnection(_formName.Trim(), _protocol, _formSiteId)
|
|
{
|
|
PrimaryConfiguration = primaryJson,
|
|
BackupConfiguration = backupJson,
|
|
FailoverRetryCount = _showBackup ? _formFailoverRetryCount : 3
|
|
};
|
|
await SiteRepository.AddDataConnectionAsync(conn);
|
|
}
|
|
await SiteRepository.SaveChangesAsync();
|
|
NavigationManager.NavigateTo("/design/connections");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_formError = $"Save failed: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
private void EnableBackup() => _showBackup = true;
|
|
|
|
private void RemoveBackup()
|
|
{
|
|
_showBackup = false;
|
|
_backupConfig = new OpcUaEndpointConfig();
|
|
_backupMx = new MxGatewayEndpointConfig();
|
|
_backupIsLegacy = false;
|
|
_formFailoverRetryCount = 3;
|
|
}
|
|
|
|
private void GoBack() => NavigationManager.NavigateTo("/design/connections");
|
|
}
|