9227d03ca8
An MTConnect driver instance could not be authored at all: RawDriverTypeDialog
(the only DriverInstance creation surface) offered a hardcoded 9-entry list
without it, and DriverConfigModal's default: branch renders a warning with no
raw-JSON fallback — so even a hand-inserted row could not be configured and
DriverConfig stayed "{}", on which ParseOptions throws "missing required
AgentUri".
Adds the typed MTConnectDriverForm (the consistent sibling pattern) covering
agentUri / deviceName / the four polling knobs / probe / reconnect, wires the
dispatch case, and offers the type in the dialog.
- All decisions live in the static MTConnectDriverForm.BuildConfigJson +
MTConnectFormModel (plain C#), because this project has no bUnit.
- The wire shape mirrors the driver's private MTConnectDriverConfigDto, NOT
MTConnectDriverOptions: the latter's TimeSpan probe knobs would serialise as
"00:00:05" and never bind to the driver's integer intervalMs.
- arch-review 01/S-6: a non-positive timing knob is REMOVED from the document
rather than written, so ParseOptions substitutes the driver's own positive
default instead of the driver faulting at Initialize on RequirePositive.
Removal (not just skipping) also stops a stale positive value surviving and
disagreeing with what the form shows.
- Unknown top-level keys survive a load/save, so the driver's hand-authored
tags[] surface is not silently deleted by opening the form.
- _jsonOpts carries JsonStringEnumConverter, satisfying the
DriverPageJsonConverterTests fleet guard properly rather than by exemption.
New RawDriverTypeDialogCoverageTests resolves the offered set through
DriverTypeNames.All, so the next registered driver fails here until the dialog
offers it.
224 lines
10 KiB
Plaintext
224 lines
10 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") · <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;
|
|
case DriverTypeNames.Sql:
|
|
<SqlDriverForm @bind-DriverConfigJson="_driverConfigJson" @bind-ResilienceConfig="_resilienceConfig" />
|
|
break;
|
|
case DriverTypeNames.Mqtt:
|
|
<MqttDriverForm @bind-DriverConfigJson="_driverConfigJson" @bind-ResilienceConfig="_resilienceConfig" />
|
|
break;
|
|
case DriverTypeNames.MTConnect:
|
|
<MTConnectDriverForm @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;
|
|
}
|
|
|
|
@* Galaxy + MQTT hold ONE connection per driver instance, so they author it here and
|
|
their device forms are informational. Every other driver splits the endpoint onto
|
|
the device (the v3 endpoint→DeviceConfig split). *@
|
|
@if (_driverType is DriverTypeNames.Galaxy or DriverTypeNames.Mqtt)
|
|
{
|
|
<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 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);
|
|
}
|
|
}
|