chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)

Group all 69 projects into category subfolders under src/ and tests/ so the
Rider Solution Explorer mirrors the module structure. Folders: Core, Server,
Drivers (with a nested Driver CLIs subfolder), Client, Tooling.

- Move every project folder on disk with git mv (history preserved as renames).
- Recompute relative paths in 57 .csproj files: cross-category ProjectReferences,
  the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external
  mxaccessgw refs in Driver.Galaxy and its test project.
- Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders.
- Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL,
  integration, install).

Build green (0 errors); unit tests pass. Docs left for a separate pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-17 01:55:28 -04:00
parent 69f02fed7f
commit a25593a9c6
1044 changed files with 365 additions and 343 deletions

View File

@@ -0,0 +1,186 @@
@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>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
{
<table class="table table-sm">
<thead><tr><th>DriverInstanceId</th><th>Name</th><th>Type</th><th>Namespace</th></tr></thead>
<tbody>
@foreach (var d in _drivers)
{
<tr>
<td><code>@d.DriverInstanceId</code></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><code>@d.NamespaceId</code></td>
</tr>
}
</tbody>
</table>
}
@if (_showForm && _namespaces is not null)
{
<div class="card">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Name</label>
<input class="form-control" @bind="_name"/>
</div>
<div class="col-md-3">
<label class="form-label">DriverType</label>
<select class="form-select" @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" @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 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) { <div class="alert alert-danger mt-3">@_error</div> }
<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>
</div>
}
@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);
}