refactor(adminui): retire generic DriverEdit.razor
All 9 driver types now have typed pages; DriverEditRouter dispatches to them directly. Unknown DriverType strings (e.g. legacy rows) render an explicit error notice instead of falling through to a generic editor — the failure mode is now visible, not silent.
This commit is contained in:
@@ -1,247 +0,0 @@
|
|||||||
@* Per Q1 of the AdminUI rebuild plan — JSON editor only, typed driver editors deferred.
|
|
||||||
DriverInstance is the keystone for everything downstream (Equipment, Tag, VirtualTag,
|
|
||||||
ScriptedAlarm all reference DriverInstanceId), so this is the second edit page after
|
|
||||||
Namespace. *@
|
|
||||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
|
||||||
@rendermode RenderMode.InteractiveServer
|
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
|
||||||
@using Microsoft.EntityFrameworkCore
|
|
||||||
@using System.ComponentModel.DataAnnotations
|
|
||||||
@using ZB.MOM.WW.OtOpcUa.Configuration
|
|
||||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
|
||||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers
|
|
||||||
@inject IDbContextFactory<OtOpcUaConfigDbContext> DbFactory
|
|
||||||
@inject NavigationManager Nav
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<h4 class="mb-0">@(IsNew ? "New driver instance" : "Edit driver instance") · <span class="mono">@ClusterId</span></h4>
|
|
||||||
<a href="/clusters/@ClusterId/drivers" class="btn btn-outline-secondary btn-sm">Cancel</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ClusterNav ClusterId="@ClusterId" ActiveTab="drivers" />
|
|
||||||
|
|
||||||
@if (!_loaded)
|
|
||||||
{
|
|
||||||
<p>Loading…</p>
|
|
||||||
}
|
|
||||||
else if (!IsNew && _existing is null)
|
|
||||||
{
|
|
||||||
<section class="panel notice rise" style="animation-delay:.02s">
|
|
||||||
Driver instance <span class="mono">@DriverInstanceId</span> was not found in cluster <span class="mono">@ClusterId</span>.
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<EditForm Model="_form" OnValidSubmit="SubmitAsync" FormName="driverEdit">
|
|
||||||
<DataAnnotationsValidator />
|
|
||||||
<DriverFormShell IsNew="IsNew" Busy="_busy" Error="_error"
|
|
||||||
CancelHref="@($"/clusters/{ClusterId}/drivers")"
|
|
||||||
OnDelete="@(IsNew ? null : (EventCallback?)EventCallback.Factory.Create(this, DeleteAsync))">
|
|
||||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="true" />
|
|
||||||
|
|
||||||
@* Driver config (JSON) — inlined; will be replaced by typed forms in Phase 4 *@
|
|
||||||
<section class="panel rise mt-3" style="animation-delay:.08s">
|
|
||||||
<div class="panel-head">Driver config (JSON)</div>
|
|
||||||
<div style="padding:1rem">
|
|
||||||
<InputTextArea @bind-Value="_form.DriverConfig" rows="12"
|
|
||||||
class="form-control form-control-sm mono"
|
|
||||||
placeholder='{ "endpoint": "10.0.0.42:502", "slaveId": 1 }' />
|
|
||||||
<div class="form-text">Schemaless per driver type — validated server-side at deploy time. JSON is reformatted on save.</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<DriverResilienceSection @bind-ResilienceConfig="_form.ResilienceConfig" />
|
|
||||||
</DriverFormShell>
|
|
||||||
</EditForm>
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public string ClusterId { get; set; } = "";
|
|
||||||
[Parameter] public string? DriverInstanceId { get; set; }
|
|
||||||
|
|
||||||
private bool IsNew => string.IsNullOrEmpty(DriverInstanceId);
|
|
||||||
|
|
||||||
private DriverIdentitySection.DriverIdentityModel _identityModel = new();
|
|
||||||
private FormModel _form = new();
|
|
||||||
private DriverInstance? _existing;
|
|
||||||
private List<Namespace> _namespaces = new();
|
|
||||||
private bool _loaded;
|
|
||||||
private bool _busy;
|
|
||||||
private string? _error;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
await using var db = await DbFactory.CreateDbContextAsync();
|
|
||||||
_namespaces = await db.Namespaces.AsNoTracking()
|
|
||||||
.Where(n => n.ClusterId == ClusterId)
|
|
||||||
.OrderBy(n => n.NamespaceId)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
if (IsNew)
|
|
||||||
{
|
|
||||||
_identityModel = new DriverIdentitySection.DriverIdentityModel
|
|
||||||
{
|
|
||||||
DriverInstanceId = "",
|
|
||||||
Name = "",
|
|
||||||
DriverType = "ModbusTcp",
|
|
||||||
NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "",
|
|
||||||
Enabled = true,
|
|
||||||
};
|
|
||||||
_form = new FormModel
|
|
||||||
{
|
|
||||||
DriverConfig = "{}",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_existing = await db.DriverInstances.AsNoTracking()
|
|
||||||
.FirstOrDefaultAsync(d => d.ClusterId == ClusterId && d.DriverInstanceId == DriverInstanceId);
|
|
||||||
if (_existing is not null)
|
|
||||||
{
|
|
||||||
_identityModel = new DriverIdentitySection.DriverIdentityModel
|
|
||||||
{
|
|
||||||
DriverInstanceId = _existing.DriverInstanceId,
|
|
||||||
Name = _existing.Name,
|
|
||||||
DriverType = _existing.DriverType,
|
|
||||||
NamespaceId = _existing.NamespaceId,
|
|
||||||
Enabled = _existing.Enabled,
|
|
||||||
};
|
|
||||||
_form = new FormModel
|
|
||||||
{
|
|
||||||
DriverConfig = _existing.DriverConfig,
|
|
||||||
ResilienceConfig = _existing.ResilienceConfig,
|
|
||||||
RowVersion = _existing.RowVersion,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_loaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SubmitAsync()
|
|
||||||
{
|
|
||||||
_busy = true;
|
|
||||||
_error = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var normalizedConfig = NormalizeJson(_form.DriverConfig);
|
|
||||||
if (normalizedConfig is null)
|
|
||||||
{
|
|
||||||
_error = "DriverConfig is not valid JSON.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var normalizedResilience = NormalizeOptionalJson(_form.ResilienceConfig);
|
|
||||||
if (!string.IsNullOrWhiteSpace(_form.ResilienceConfig) && normalizedResilience is null)
|
|
||||||
{
|
|
||||||
_error = "ResilienceConfig is not valid JSON. Leave blank to use defaults.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await using var db = await DbFactory.CreateDbContextAsync();
|
|
||||||
if (IsNew)
|
|
||||||
{
|
|
||||||
if (await db.DriverInstances.AnyAsync(d => d.DriverInstanceId == _identityModel.DriverInstanceId))
|
|
||||||
{
|
|
||||||
_error = $"Driver instance '{_identityModel.DriverInstanceId}' already exists.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
db.DriverInstances.Add(new DriverInstance
|
|
||||||
{
|
|
||||||
DriverInstanceId = _identityModel.DriverInstanceId,
|
|
||||||
ClusterId = ClusterId,
|
|
||||||
NamespaceId = _identityModel.NamespaceId,
|
|
||||||
Name = _identityModel.Name,
|
|
||||||
DriverType = _identityModel.DriverType,
|
|
||||||
Enabled = _identityModel.Enabled,
|
|
||||||
DriverConfig = normalizedConfig,
|
|
||||||
ResilienceConfig = normalizedResilience,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var entity = await db.DriverInstances.FirstOrDefaultAsync(
|
|
||||||
d => d.ClusterId == ClusterId && d.DriverInstanceId == DriverInstanceId);
|
|
||||||
if (entity is null)
|
|
||||||
{
|
|
||||||
_error = "Row no longer exists.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion;
|
|
||||||
entity.NamespaceId = _identityModel.NamespaceId;
|
|
||||||
entity.Name = _identityModel.Name;
|
|
||||||
entity.Enabled = _identityModel.Enabled;
|
|
||||||
entity.DriverConfig = normalizedConfig;
|
|
||||||
entity.ResilienceConfig = normalizedResilience;
|
|
||||||
}
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
Nav.NavigateTo($"/clusters/{ClusterId}/drivers");
|
|
||||||
}
|
|
||||||
catch (DbUpdateConcurrencyException)
|
|
||||||
{
|
|
||||||
_error = "Another user changed this driver instance while you were editing. Reload to see the latest values, then re-apply your changes.";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_error = ex.Message;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_busy = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeleteAsync()
|
|
||||||
{
|
|
||||||
if (IsNew) return;
|
|
||||||
_busy = true;
|
|
||||||
_error = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await using var db = await DbFactory.CreateDbContextAsync();
|
|
||||||
var entity = await db.DriverInstances.FirstOrDefaultAsync(
|
|
||||||
d => d.ClusterId == ClusterId && d.DriverInstanceId == DriverInstanceId);
|
|
||||||
if (entity is null)
|
|
||||||
{
|
|
||||||
Nav.NavigateTo($"/clusters/{ClusterId}/drivers");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion;
|
|
||||||
db.DriverInstances.Remove(entity);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
Nav.NavigateTo($"/clusters/{ClusterId}/drivers");
|
|
||||||
}
|
|
||||||
catch (DbUpdateConcurrencyException)
|
|
||||||
{
|
|
||||||
_error = "Another user changed this driver instance while you were viewing it. Reload before deleting.";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_error = $"Delete failed: {ex.Message}. (Likely because equipment/tags still reference this driver — remove them first.)";
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_busy = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? NormalizeJson(string? input)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(input)) return null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var doc = System.Text.Json.JsonDocument.Parse(input);
|
|
||||||
return System.Text.Json.JsonSerializer.Serialize(doc.RootElement);
|
|
||||||
}
|
|
||||||
catch { return null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? NormalizeOptionalJson(string? input) =>
|
|
||||||
string.IsNullOrWhiteSpace(input) ? null : NormalizeJson(input);
|
|
||||||
|
|
||||||
private sealed class FormModel
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
public string DriverConfig { get; set; } = "{}";
|
|
||||||
public string? ResilienceConfig { get; set; }
|
|
||||||
public byte[] RowVersion { get; set; } = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+16
-5
@@ -26,9 +26,22 @@ else if (_existing is null)
|
|||||||
Driver instance <span class="mono">@DriverInstanceId</span> was not found in cluster <span class="mono">@ClusterId</span>.
|
Driver instance <span class="mono">@DriverInstanceId</span> was not found in cluster <span class="mono">@ClusterId</span>.
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
else if (ResolveComponentType() is { } pageType)
|
||||||
|
{
|
||||||
|
<DynamicComponent Type="pageType" Parameters="BuildParameters()" />
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<DynamicComponent Type="ResolveComponentType()" Parameters="BuildParameters()" />
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">Edit driver instance · <span class="mono">@ClusterId</span></h4>
|
||||||
|
<a href="/clusters/@ClusterId/drivers" class="btn btn-outline-secondary btn-sm">Cancel</a>
|
||||||
|
</div>
|
||||||
|
<ClusterNav ClusterId="@ClusterId" ActiveTab="drivers" />
|
||||||
|
<section class="panel notice rise" style="animation-delay:.02s; border-color:var(--alert)">
|
||||||
|
Driver instance <span class="mono">@DriverInstanceId</span> has an unknown <code>DriverType</code> value of
|
||||||
|
<strong><span class="mono">@_existing.DriverType</span></strong>. No editor is registered for this type.
|
||||||
|
Likely causes: the row was written by a newer build, or the type-string was corrupted in the database.
|
||||||
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
@@ -60,10 +73,8 @@ else
|
|||||||
_loaded = true;
|
_loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Type ResolveComponentType()
|
private Type? ResolveComponentType()
|
||||||
=> _componentMap.TryGetValue(_existing!.DriverType, out var t)
|
=> _componentMap.TryGetValue(_existing!.DriverType, out var t) ? t : null;
|
||||||
? t
|
|
||||||
: typeof(ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.DriverEdit);
|
|
||||||
|
|
||||||
private IDictionary<string, object> BuildParameters()
|
private IDictionary<string, object> BuildParameters()
|
||||||
=> new Dictionary<string, object>
|
=> new Dictionary<string, object>
|
||||||
|
|||||||
Reference in New Issue
Block a user