Adopt the technical-light design system across the Admin web UI: - Vendor theme.css + IBM Plex woff2 fonts into wwwroot; include theme.css globally after Bootstrap. - Rebuild MainLayout: top app-bar (brand mark, breadcrumb, connection pill) + hairline-ruled side rail with accent-bordered active link. - Convert all 33 pages to the component catalog — tables to panel + data-table (num/mono columns), KPI cards to agg-grid, detail blocks to metric-card/kv rows, badges to chips, alerts to panel notice, headings to page-title/panel-head, .rise reveals. - Buttons/forms stay on Bootstrap; theme.css restyles them via --bs-* overrides. View-specific layout lives in app.css; all colour/type comes from theme.css tokens. Also fix a pre-existing /fleet 500: the node-state query ordered on a property of a constructed FleetNodeRow record, which EF Core cannot translate. Order the join's columns before projecting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
193 lines
8.5 KiB
Plaintext
193 lines
8.5 KiB
Plaintext
@using System.Text.Json
|
|
@using ZB.MOM.WW.OtOpcUa.Admin.Components.Pages.Modbus
|
|
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
|
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
|
@inject DriverInstanceService DriverSvc
|
|
@inject NamespaceService NsSvc
|
|
|
|
<div class="d-flex justify-content-between mb-3">
|
|
<h4 class="panel-head">DriverInstances</h4>
|
|
<button class="btn btn-sm btn-primary" @onclick="() => _showForm = true">Add driver</button>
|
|
</div>
|
|
|
|
@if (_drivers is null) { <p>Loading…</p> }
|
|
else if (_drivers.Count == 0) { <p class="text-muted">No drivers configured in this draft.</p> }
|
|
else
|
|
{
|
|
<section class="panel rise" style="animation-delay:.02s">
|
|
<div class="panel-head">Configured drivers</div>
|
|
<div class="table-wrap">
|
|
<table class="data-table">
|
|
<thead><tr><th>DriverInstanceId</th><th>Name</th><th>Type</th><th>Namespace</th></tr></thead>
|
|
<tbody>
|
|
@foreach (var d in _drivers)
|
|
{
|
|
<tr>
|
|
<td><span class="mono">@d.DriverInstanceId</span></td>
|
|
<td>@d.Name</td>
|
|
<td>
|
|
@if (string.Equals(d.DriverType, "Focas", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
<a href="/drivers/focas/@d.DriverInstanceId">@d.DriverType</a>
|
|
}
|
|
else
|
|
{
|
|
@d.DriverType
|
|
}
|
|
</td>
|
|
<td><span class="mono">@d.NamespaceId</span></td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
@if (_showForm && _namespaces is not null)
|
|
{
|
|
<section class="panel rise" style="animation-delay:.08s">
|
|
<div class="panel-head">Add driver</div>
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-3">
|
|
<label class="form-label">Name</label>
|
|
<input class="form-control form-control-sm" @bind="_name"/>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">DriverType</label>
|
|
<select class="form-select form-select-sm" @bind="_type">
|
|
<option>Galaxy</option>
|
|
<option>Modbus</option>
|
|
<option>AbCip</option>
|
|
<option>AbLegacy</option>
|
|
<option>S7</option>
|
|
<option>Focas</option>
|
|
<option>OpcUaClient</option>
|
|
</select>
|
|
<div class="form-text">Type string must match the driver's registered factory name; this dropdown wraps the canonical names.</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Namespace</label>
|
|
<select class="form-select form-select-sm" @bind="_nsId">
|
|
@foreach (var n in _namespaces) { <option value="@n.NamespaceId">@n.Kind — @n.NamespaceUri</option> }
|
|
</select>
|
|
</div>
|
|
<div class="col-12">
|
|
@if (string.Equals(_type, "Modbus", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
@* #147 — typed editor for Modbus drivers. The generic textarea is a fall-back
|
|
for driver types that haven't yet shipped a typed editor. *@
|
|
<label class="form-label">Modbus options (typed editor)</label>
|
|
<ModbusOptionsEditor Model="_modbusOptions"/>
|
|
}
|
|
else
|
|
{
|
|
<label class="form-label">DriverConfig JSON (schemaless per driver type)</label>
|
|
<textarea class="form-control form-control-sm font-monospace" rows="6" @bind="_config"></textarea>
|
|
<div class="form-text">Phase 1: generic JSON editor — per-driver schema validation arrives in each driver's phase (decision #94).</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
@if (_error is not null) { <section class="panel notice mt-3">@_error</section> }
|
|
<div class="mt-3">
|
|
<button class="btn btn-sm btn-primary" @onclick="SaveAsync">Save</button>
|
|
<button class="btn btn-sm btn-secondary ms-2" @onclick="() => _showForm = false">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
@code {
|
|
[Parameter] public long GenerationId { get; set; }
|
|
[Parameter] public string ClusterId { get; set; } = string.Empty;
|
|
|
|
private List<DriverInstance>? _drivers;
|
|
private List<Namespace>? _namespaces;
|
|
private bool _showForm;
|
|
private string _name = string.Empty;
|
|
private string _type = "Modbus";
|
|
private string _nsId = string.Empty;
|
|
private string _config = "{}";
|
|
private string? _error;
|
|
|
|
// #147 — typed editor model for Modbus drivers. Defaults match ModbusDriverOptions
|
|
// defaults so an unedited form produces config equivalent to the historical
|
|
// pre-typed-editor wire output. Serialised to _config on Save when type=Modbus.
|
|
private ModbusOptionsEditor.ModbusOptionsViewModel _modbusOptions = new();
|
|
private static readonly JsonSerializerOptions ModbusJsonOptions = new() { WriteIndented = true };
|
|
|
|
protected override async Task OnParametersSetAsync() => await ReloadAsync();
|
|
|
|
private async Task ReloadAsync()
|
|
{
|
|
_drivers = await DriverSvc.ListAsync(GenerationId, CancellationToken.None);
|
|
_namespaces = await NsSvc.ListAsync(GenerationId, CancellationToken.None);
|
|
_nsId = _namespaces.FirstOrDefault()?.NamespaceId ?? string.Empty;
|
|
}
|
|
|
|
private async Task SaveAsync()
|
|
{
|
|
_error = null;
|
|
if (string.IsNullOrWhiteSpace(_name) || string.IsNullOrWhiteSpace(_nsId))
|
|
{
|
|
_error = "Name and Namespace are required";
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
// #147 — for Modbus drivers serialize the typed editor model into the DriverConfig
|
|
// JSON column. Other driver types still use the raw textarea contents until each
|
|
// ships its own typed editor (decision #94 — per-driver schema validation arrives
|
|
// per driver phase).
|
|
var configJson = string.Equals(_type, "Modbus", StringComparison.OrdinalIgnoreCase)
|
|
? SerializeModbusOptions(_modbusOptions)
|
|
: _config;
|
|
|
|
await DriverSvc.AddAsync(GenerationId, ClusterId, _nsId, _name, _type, configJson, CancellationToken.None);
|
|
_name = string.Empty; _config = "{}";
|
|
_modbusOptions = new();
|
|
_showForm = false;
|
|
await ReloadAsync();
|
|
}
|
|
catch (Exception ex) { _error = ex.Message; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the view-model field names onto the JSON shape <c>ModbusDriverFactoryExtensions</c>
|
|
/// consumes. Hand-rolled because the DTO uses millisecond / byte field flavours that the
|
|
/// view model exposes as TimeSpan-derived integers; a System.Text.Json round-trip would
|
|
/// emit the .NET-native names instead.
|
|
/// </summary>
|
|
private static string SerializeModbusOptions(ModbusOptionsEditor.ModbusOptionsViewModel m) =>
|
|
JsonSerializer.Serialize(new
|
|
{
|
|
host = m.Host,
|
|
port = m.Port,
|
|
unitId = m.UnitId,
|
|
family = m.Family.ToString(),
|
|
melsecSubFamily = m.MelsecSubFamily.ToString(),
|
|
keepAlive = new
|
|
{
|
|
enabled = m.KeepAliveEnabled,
|
|
timeMs = m.KeepAliveTimeSec * 1000,
|
|
intervalMs = m.KeepAliveIntervalSec * 1000,
|
|
retryCount = m.KeepAliveRetryCount,
|
|
},
|
|
reconnect = new
|
|
{
|
|
initialDelayMs = m.ReconnectInitialDelayMs,
|
|
maxDelayMs = m.ReconnectMaxDelayMs,
|
|
backoffMultiplier = m.ReconnectBackoffMultiplier,
|
|
},
|
|
maxRegistersPerRead = m.MaxRegistersPerRead,
|
|
maxRegistersPerWrite = m.MaxRegistersPerWrite,
|
|
maxCoilsPerRead = m.MaxCoilsPerRead,
|
|
maxReadGap = m.MaxReadGap,
|
|
useFC15ForSingleCoilWrites = m.UseFC15ForSingleCoilWrites,
|
|
useFC16ForSingleRegisterWrites = m.UseFC16ForSingleRegisterWrites,
|
|
writeOnChangeOnly = m.WriteOnChangeOnly,
|
|
tags = Array.Empty<object>(),
|
|
}, ModbusJsonOptions);
|
|
}
|