Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverConfigModal.razor
T

221 lines
9.7 KiB
Plaintext

@* Create / edit modal for a raw-tree DriverInstance's channel/protocol config (endpoint lives on the
device — see DeviceModal). Dispatches to the right per-driver form body by DriverType; the form body
carries its own resilience section. The coordinator wires this into RawTree via @bind-Visible. *@
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Forms
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
@using ZB.MOM.WW.OtOpcUa.Core.Abstractions
@inject IRawTreeService Svc
@if (Visible)
{
<div class="modal-backdrop fade show" style="display:block"></div>
<div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(_isNew ? "New driver" : "Configure driver") &middot; <span class="mono">@_driverType</span></h5>
<button type="button" class="btn-close" aria-label="Close" @onclick="CloseAsync"></button>
</div>
<div class="modal-body">
@if (_loadError is not null)
{
<div class="panel notice notice-alert mb-3">@_loadError</div>
}
else
{
<div class="row g-3 mb-2">
<div class="col-md-6">
<label class="form-label">Driver name</label>
<input class="form-control form-control-sm mono" @bind="_name" placeholder="line3-modbus" />
<div class="form-text">A RawPath segment — letters, digits, dash, underscore.</div>
</div>
</div>
@* Rendered from DriverConfigFormMap rather than a @switch: a Razor switch compiles
into BuildRenderTree's IL, so no test can enumerate its cases — which is how this
modal came to be missing a Calculation arm while the driver-picker guard stayed
green (deferment.md G-1/G-4). The map is guarded by DriverFormMapParityTests. *@
@if (DriverConfigFormMap.Resolve(_driverType) is { } formType)
{
<DynamicComponent Type="formType" Parameters="_formParameters" />
}
else
{
<div class="alert alert-warning">No typed config form for driver type <span class="mono">@_driverType</span>.</div>
}
@* Some drivers hold ONE connection per driver instance and author it here; the rest
split the endpoint onto the device (the v3 endpoint→DeviceConfig split). The set is
DeviceFormMap.SingleConnectionDriverTypes — this used to hardcode Galaxy-or-Mqtt and
so told Sql and MTConnect authors to go find an endpoint field on a device form that
does not exist (deferment.md G-3). *@
@if (DeviceFormMap.IsSingleConnection(_driverType))
{
<p class="form-text mt-3 mb-0">
This driver holds a single connection, authored above. Test-connect lives on its
<strong>device</strong> — open the device modal to verify connectivity.
</p>
}
else
{
<p class="form-text mt-3 mb-0">
The connection endpoint + Test-connect live on the driver's <strong>device</strong> — open the device modal to author host/port and verify connectivity.
</p>
}
@if (_saveError is not null)
{
<div class="panel notice notice-alert mt-3">@_saveError</div>
}
}
</div>
<div class="modal-footer">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary" @onclick="CloseAsync" disabled="@_busy">Cancel</button>
@if (_loadError is null)
{
<button type="button" class="btn btn-primary" @onclick="SaveAsync" disabled="@_busy">
@if (_busy) { <span class="spinner-border spinner-border-sm me-1"></span> }
@(_isNew ? "Create driver" : "Save changes")
</button>
}
</div>
</div>
</div>
</div>
</div>
}
@code {
/// <summary>Whether the modal is shown (supports @bind-Visible).</summary>
[Parameter] public bool Visible { get; set; }
/// <summary>Two-way visibility callback.</summary>
[Parameter] public EventCallback<bool> VisibleChanged { get; set; }
/// <summary>The driver instance to edit; null ⇒ create a new driver.</summary>
[Parameter] public string? DriverInstanceId { get; set; }
// --- Create-mode inputs (ignored on edit) ---
/// <summary>The owning cluster (required for create).</summary>
[Parameter] public string? ClusterId { get; set; }
/// <summary>The containing folder, or null for a cluster-root driver (create).</summary>
[Parameter] public string? FolderId { get; set; }
/// <summary>The driver type to create (required for create; immutable on edit).</summary>
[Parameter] public string? DriverType { get; set; }
/// <summary>Raised after a successful create/update so the tree can refresh.</summary>
[Parameter] public EventCallback OnSaved { get; set; }
private bool _isNew;
private string _driverType = "";
private string _name = "";
private string _driverConfigJson = "{}";
/// <summary>Parameters handed to the DynamicComponent-rendered driver form. Rebuilt on every render so
/// the child receives the CURRENT json — a cached dictionary would pin the value captured at first
/// render and silently strand every subsequent edit.</summary>
private Dictionary<string, object> _formParameters => new()
{
["DriverConfigJson"] = _driverConfigJson,
["DriverConfigJsonChanged"] = EventCallback.Factory.Create<string>(this, v => _driverConfigJson = v),
["ResilienceConfig"] = _resilienceConfig!,
["ResilienceConfigChanged"] = EventCallback.Factory.Create<string?>(this, v => _resilienceConfig = v),
};
private string? _resilienceConfig;
private byte[] _rowVersion = [];
private bool _busy;
private string? _loadError;
private string? _saveError;
private bool _openHandled;
// Bumped on every open and close; LoadAsync re-checks it after each await so a superseded open can't
// install one target's fields under another target's displayed modal (RawBrowseModal idiom).
private int _openGeneration;
private bool IsStale(int generation) => generation != _openGeneration || !Visible;
protected override async Task OnParametersSetAsync()
{
if (Visible && !_openHandled)
{
_openHandled = true;
await LoadAsync(++_openGeneration);
}
else if (!Visible)
{
_openHandled = false;
_openGeneration++;
}
}
private async Task LoadAsync(int generation)
{
_busy = false; _loadError = null; _saveError = null;
_isNew = string.IsNullOrEmpty(DriverInstanceId);
if (_isNew)
{
_driverType = DriverType ?? "";
_name = "";
_driverConfigJson = "{}";
_resilienceConfig = null;
_rowVersion = [];
}
else
{
var dto = await Svc.LoadDriverForEditAsync(DriverInstanceId!);
if (IsStale(generation)) { return; }
if (dto is null) { _loadError = $"Driver '{DriverInstanceId}' was not found."; return; }
_driverType = dto.DriverType;
_name = dto.Name;
_driverConfigJson = string.IsNullOrWhiteSpace(dto.DriverConfig) ? "{}" : dto.DriverConfig;
_resilienceConfig = dto.ResilienceConfig;
_rowVersion = dto.RowVersion;
}
}
private async Task SaveAsync()
{
_busy = true; _saveError = null;
try
{
UnsMutationResult result;
if (_isNew)
{
if (string.IsNullOrEmpty(ClusterId))
{
_saveError = "No owning cluster was supplied.";
return;
}
result = await Svc.CreateDriverAsync(ClusterId, FolderId, _name, _driverType, _driverConfigJson);
}
else
{
result = await Svc.UpdateDriverAsync(DriverInstanceId!, _name, _driverConfigJson, _resilienceConfig, _rowVersion);
}
if (!result.Ok)
{
_saveError = result.Error ?? "Save failed.";
return;
}
await OnSaved.InvokeAsync();
await SetVisibleAsync(false);
}
catch (Exception ex) { _saveError = ex.Message; }
finally { _busy = false; }
}
private Task CloseAsync() => SetVisibleAsync(false);
private async Task SetVisibleAsync(bool value)
{
Visible = value;
_openHandled = value;
_openGeneration++;
await VisibleChanged.InvokeAsync(value);
}
}