3d081ad29c
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
223 lines
9.7 KiB
Plaintext
223 lines
9.7 KiB
Plaintext
@* Create / edit modal for a raw-tree Device's connection endpoint (v3 endpoint→DeviceConfig split).
|
|
Dispatches to the right per-driver device form by DriverType. Test-connect builds the merged
|
|
Driver+Device config transiently (so unsaved edits probe correctly), via DriverDeviceConfigMerger —
|
|
the same merge LoadMergedProbeConfigAsync uses. The coordinator wires this into RawTree via @bind-Visible. *@
|
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.DeviceForms
|
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
|
@using ZB.MOM.WW.OtOpcUa.Commons.Types
|
|
@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-lg" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">@(_isNew ? "New device" : "Edit device") · <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">Device name</label>
|
|
<input class="form-control form-control-sm mono" @bind="_name" placeholder="e.g. plc1" />
|
|
<div class="form-text">A RawPath segment — letters, digits, dash, underscore.</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="form-check form-switch mt-4">
|
|
<input type="checkbox" class="form-check-input" id="devEnabled" @bind="_enabled" />
|
|
<label class="form-check-label" for="devEnabled">Enabled</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@* Rendered from DeviceFormMap — see DriverConfigFormMap for why this is a map and
|
|
not a @switch. Drivers in DeviceFormMap.SingleConnectionDriverTypes legitimately
|
|
have no per-device form; the parity test consults that set. *@
|
|
@if (DeviceFormMap.Resolve(_driverType) is { } deviceFormType)
|
|
{
|
|
<DynamicComponent Type="deviceFormType" Parameters="_deviceFormParameters" />
|
|
}
|
|
else if (DeviceFormMap.IsSingleConnection(_driverType))
|
|
{
|
|
<div class="alert alert-info">
|
|
This driver holds a single connection, authored on the <strong>driver</strong>.
|
|
There is nothing to configure per device.
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="alert alert-warning">No typed device form for driver type <span class="mono">@_driverType</span>.</div>
|
|
}
|
|
|
|
<div class="mt-3">
|
|
<DriverTestConnectButton DriverType="@_driverType" GetConfigJson="@BuildMergedProbeConfig" />
|
|
</div>
|
|
|
|
@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 device" : "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 device to edit; null ⇒ create a new device under <see cref="DriverInstanceId"/>.</summary>
|
|
[Parameter] public string? DeviceId { get; set; }
|
|
/// <summary>The owning driver instance (required for create; resolved from the device on edit).</summary>
|
|
[Parameter] public string? DriverInstanceId { get; set; }
|
|
/// <summary>The driver type (required for create; joined in from the device 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? _resolvedDriverInstanceId;
|
|
private string _name = "";
|
|
private bool _enabled = true;
|
|
private string _deviceConfigJson = "{}";
|
|
|
|
/// <summary>Parameters handed to the DynamicComponent-rendered device form. Rebuilt on every render —
|
|
/// see DriverConfigModal for why this must not be cached.</summary>
|
|
private Dictionary<string, object> _deviceFormParameters => new()
|
|
{
|
|
["DeviceConfigJson"] = _deviceConfigJson,
|
|
["DeviceConfigJsonChanged"] = EventCallback.Factory.Create<string>(this, v => _deviceConfigJson = v),
|
|
};
|
|
private byte[] _rowVersion = [];
|
|
private string _parentDriverConfig = "{}";
|
|
|
|
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(DeviceId);
|
|
|
|
if (_isNew)
|
|
{
|
|
_resolvedDriverInstanceId = DriverInstanceId;
|
|
_driverType = DriverType ?? "";
|
|
_name = ""; // placeholder only: a pre-filled value made typing append to it (#577); the name is validated on save
|
|
_enabled = true;
|
|
_deviceConfigJson = "{}";
|
|
_rowVersion = [];
|
|
}
|
|
else
|
|
{
|
|
var dto = await Svc.LoadDeviceForEditAsync(DeviceId!);
|
|
if (dto is null) { _loadError = $"Device '{DeviceId}' was not found."; return; }
|
|
_resolvedDriverInstanceId = dto.DriverInstanceId;
|
|
_driverType = dto.DriverType;
|
|
_name = dto.Name;
|
|
_enabled = dto.Enabled;
|
|
_deviceConfigJson = string.IsNullOrWhiteSpace(dto.DeviceConfig) ? "{}" : dto.DeviceConfig;
|
|
_rowVersion = dto.RowVersion;
|
|
}
|
|
|
|
// The parent driver's DriverConfig — folded with the (possibly unsaved) DeviceConfig for Test-connect.
|
|
if (!string.IsNullOrEmpty(_resolvedDriverInstanceId))
|
|
{
|
|
var drv = await Svc.LoadDriverForEditAsync(_resolvedDriverInstanceId);
|
|
_parentDriverConfig = drv?.DriverConfig ?? "{}";
|
|
}
|
|
}
|
|
|
|
/// <summary>Builds the merged Driver+Device probe config from the CURRENT (possibly unsaved) form —
|
|
/// same merge as <c>LoadMergedProbeConfigAsync</c>, so Test-connect works before the device is saved.</summary>
|
|
private string BuildMergedProbeConfig()
|
|
=> DriverDeviceConfigMerger.Merge(
|
|
_parentDriverConfig,
|
|
new[] { new DriverDeviceConfigMerger.DeviceRow(_name, _deviceConfigJson) },
|
|
Array.Empty<RawTagEntry>());
|
|
|
|
private async Task SaveAsync()
|
|
{
|
|
_busy = true; _saveError = null;
|
|
try
|
|
{
|
|
UnsMutationResult result;
|
|
if (_isNew)
|
|
{
|
|
if (string.IsNullOrEmpty(_resolvedDriverInstanceId))
|
|
{
|
|
_saveError = "No owning driver instance was supplied.";
|
|
return;
|
|
}
|
|
result = await Svc.CreateDeviceAsync(_resolvedDriverInstanceId, _name, _deviceConfigJson);
|
|
}
|
|
else
|
|
{
|
|
result = await Svc.UpdateDeviceAsync(DeviceId!, _name, _deviceConfigJson, _enabled, _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);
|
|
}
|
|
}
|