feat(adminui): B2-WP3 driver/device config modals + page-to-form refactor
Extract the 8 typed driver pages into embeddable <Driver>DriverForm bodies (channel/protocol config) and add per-driver <Driver>DeviceForm bodies (connection endpoint -> Device.DeviceConfig, v3 endpoint split). New DriverConfigModal + DeviceModal dispatch by DriverType (DriverTypeNames) for the /raw tree; Test-connect inside DeviceModal builds the merged Driver+Device config transiently via DriverDeviceConfigMerger. Routed pages now host the form bodies and keep working. Round-trip + enum-serialization guard tests for the Modbus driver form + Modbus device model; retargeted the existing page-form serialization tests + the _jsonOpts converter guard to the forms. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
+201
@@ -0,0 +1,201 @@
|
||||
@* 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") · <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 mb-3" style="border-color:var(--alert)">@_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>
|
||||
|
||||
@switch (_driverType)
|
||||
{
|
||||
case DriverTypeNames.Modbus:
|
||||
<ModbusDriverForm @bind-DriverConfigJson="_driverConfigJson" @bind-ResilienceConfig="_resilienceConfig" />
|
||||
break;
|
||||
case DriverTypeNames.S7:
|
||||
<S7DriverForm @bind-DriverConfigJson="_driverConfigJson" @bind-ResilienceConfig="_resilienceConfig" />
|
||||
break;
|
||||
case DriverTypeNames.AbCip:
|
||||
<AbCipDriverForm @bind-DriverConfigJson="_driverConfigJson" @bind-ResilienceConfig="_resilienceConfig" />
|
||||
break;
|
||||
case DriverTypeNames.AbLegacy:
|
||||
<AbLegacyDriverForm @bind-DriverConfigJson="_driverConfigJson" @bind-ResilienceConfig="_resilienceConfig" />
|
||||
break;
|
||||
case DriverTypeNames.TwinCAT:
|
||||
<TwinCATDriverForm @bind-DriverConfigJson="_driverConfigJson" @bind-ResilienceConfig="_resilienceConfig" />
|
||||
break;
|
||||
case DriverTypeNames.FOCAS:
|
||||
<FocasDriverForm @bind-DriverConfigJson="_driverConfigJson" @bind-ResilienceConfig="_resilienceConfig" />
|
||||
break;
|
||||
case DriverTypeNames.OpcUaClient:
|
||||
<OpcUaClientDriverForm @bind-DriverConfigJson="_driverConfigJson" @bind-ResilienceConfig="_resilienceConfig" />
|
||||
break;
|
||||
case DriverTypeNames.Galaxy:
|
||||
<GalaxyDriverForm @bind-DriverConfigJson="_driverConfigJson" @bind-ResilienceConfig="_resilienceConfig" />
|
||||
break;
|
||||
default:
|
||||
<div class="alert alert-warning">No typed config form for driver type <span class="mono">@_driverType</span>.</div>
|
||||
break;
|
||||
}
|
||||
|
||||
<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 mt-3" style="border-color:var(--alert)">@_saveError</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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>
|
||||
}
|
||||
|
||||
@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 = "{}";
|
||||
private string? _resilienceConfig;
|
||||
private byte[] _rowVersion = [];
|
||||
|
||||
private bool _busy;
|
||||
private string? _loadError;
|
||||
private string? _saveError;
|
||||
private bool _openHandled;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (Visible && !_openHandled)
|
||||
{
|
||||
_openHandled = true;
|
||||
await LoadAsync();
|
||||
}
|
||||
else if (!Visible)
|
||||
{
|
||||
_openHandled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
_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 (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;
|
||||
await VisibleChanged.InvokeAsync(value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user