27b3a014da
Removes both @page directives from DriverEdit.razor. The picker owns
/drivers/new; the router owns /drivers/{id} and dispatches via
DynamicComponent (currently falls back to DriverEdit for every driver
type — Phase 4 populates the type map one driver at a time).
248 lines
9.5 KiB
Plaintext
248 lines
9.5 KiB
Plaintext
@* 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; } = [];
|
|
}
|
|
}
|