feat(m9/T32c): schema-library CRUD commands + handlers + Central UI page + read-accessor

This commit is contained in:
Joseph Doherty
2026-06-18 12:32:31 -04:00
parent 71d5722692
commit 71a2bca4df
13 changed files with 1363 additions and 0 deletions
@@ -31,6 +31,7 @@
<NavRailSection Title="Design" Key="design">
<NavRailItem Href="/design/templates" Text="Templates" />
<NavRailItem Href="/design/shared-scripts" Text="Shared Scripts" />
<NavRailItem Href="/design/schema-library" Text="Schema Library" />
<NavRailItem Href="/design/connections" Text="Connections" />
<NavRailItem Href="/design/external-systems" Text="External Systems" />
<NavRailItem Href="/design/transport/export" Text="Export Bundle" />
@@ -0,0 +1,228 @@
@page "/design/schema-library"
@using ZB.MOM.WW.ScadaBridge.Security
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Schemas
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
@inject ISchemaLibraryService SchemaLibraryService
@inject IDialogService Dialog
@* Schema Library (M9-T32c): list + create/edit (via SchemaBuilder) + delete the
reusable named JSON-Schema library entries that the {"$ref":"lib:Name"} resolver
(T32b) resolves against. Every mutation is dispatched through ISchemaLibraryService
— the guard-running ManagementActor path — never a direct repo write. *@
<div class="container-fluid mt-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">Schema Library</h4>
@if (!_editing)
{
<button class="btn btn-primary btn-sm" @onclick="BeginCreate">New Schema</button>
}
</div>
<ToastNotification @ref="_toast" />
@if (_editing)
{
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title mb-3">@(_editId.HasValue ? $"Edit Schema: {_editOriginalName}" : "New Schema")</h5>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label small">Name</label>
<input type="text" class="form-control form-control-sm"
placeholder="e.g. Address" @bind="_formName" />
<div class="form-text">Referenced as <code>lib:@(string.IsNullOrWhiteSpace(_formName) ? "Name" : _formName.Trim())</code>.</div>
</div>
<div class="col-md-6">
<label class="form-label small">Scope <span class="text-muted">(optional)</span></label>
<input type="text" class="form-control form-control-sm"
placeholder="(global)" @bind="_formScope" />
</div>
</div>
<label class="form-label small">Schema</label>
<SchemaBuilder Mode="value"
Value="@_formSchemaJson"
ValueChanged="@(v => _formSchemaJson = v)" />
@if (_formError != null)
{
<div class="text-danger small mt-2">@_formError</div>
}
<div class="mt-3">
<button class="btn btn-success btn-sm me-1" @onclick="SaveSchema" disabled="@_busy">Save</button>
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelEdit" disabled="@_busy">Cancel</button>
</div>
</div>
</div>
}
@if (_loading)
{
<LoadingSpinner IsLoading="true" />
}
else if (_schemas.Count == 0)
{
<div class="text-center py-5 text-muted">
<p class="mb-3">No library schemas defined.</p>
@if (!_editing)
{
<button class="btn btn-primary btn-sm" @onclick="BeginCreate">Create your first schema</button>
}
</div>
}
else
{
<table class="table table-sm align-middle">
<thead>
<tr>
<th>Name</th>
<th>Scope</th>
<th>Reference</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var s in _schemas)
{
<tr @key="s.Id">
<td class="fw-semibold">@s.Name</td>
<td>@(string.IsNullOrWhiteSpace(s.Scope) ? "—" : s.Scope)</td>
<td><code>lib:@s.Name</code></td>
<td class="text-end">
<button class="btn btn-outline-primary btn-sm me-1"
@onclick="() => BeginEdit(s)">Edit</button>
<button class="btn btn-outline-danger btn-sm"
@onclick="() => DeleteSchema(s)">Delete</button>
</td>
</tr>
}
</tbody>
</table>
}
</div>
@code {
private List<SharedSchema> _schemas = new();
private bool _loading = true;
private bool _busy;
private bool _editing;
private int? _editId;
private string _editOriginalName = string.Empty;
private string _formName = string.Empty;
private string? _formScope;
private string? _formSchemaJson;
private string? _formError;
private ToastNotification _toast = default!;
protected override async Task OnInitializedAsync()
{
await LoadAsync();
}
private async Task LoadAsync()
{
_loading = true;
try
{
_schemas = (await SchemaLibraryService.ListAsync()).ToList();
}
finally
{
_loading = false;
}
}
private void BeginCreate()
{
_editing = true;
_editId = null;
_editOriginalName = string.Empty;
_formName = string.Empty;
_formScope = null;
// Seed an object-shaped schema so the builder opens with a useful default.
_formSchemaJson = """{"type":"object","properties":{}}""";
_formError = null;
}
private void BeginEdit(SharedSchema schema)
{
_editing = true;
_editId = schema.Id;
_editOriginalName = schema.Name;
_formName = schema.Name;
_formScope = schema.Scope;
_formSchemaJson = schema.SchemaJson;
_formError = null;
}
private void CancelEdit()
{
_editing = false;
_formError = null;
}
private async Task SaveSchema()
{
_formError = null;
var name = _formName?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(name))
{
_formError = "Schema name is required.";
return;
}
var scope = string.IsNullOrWhiteSpace(_formScope) ? null : _formScope.Trim();
var schemaJson = _formSchemaJson ?? string.Empty;
_busy = true;
try
{
var result = _editId.HasValue
? await SchemaLibraryService.UpdateAsync(_editId.Value, name, scope, schemaJson)
: await SchemaLibraryService.CreateAsync(name, scope, schemaJson);
if (result.Success)
{
_toast.ShowSuccess(_editId.HasValue
? $"Schema '{name}' updated."
: $"Schema '{name}' created.");
_editing = false;
await LoadAsync();
}
else
{
_formError = result.Error;
}
}
finally
{
_busy = false;
}
}
private async Task DeleteSchema(SharedSchema schema)
{
var confirmed = await Dialog.ConfirmAsync(
"Delete Schema",
$"Delete library schema '{schema.Name}'? References to lib:{schema.Name} will no longer resolve.",
danger: true);
if (!confirmed) return;
var result = await SchemaLibraryService.DeleteAsync(schema.Id);
if (result.Success)
{
_toast.ShowSuccess($"Schema '{schema.Name}' deleted.");
await LoadAsync();
}
else
{
_toast.ShowError(result.Error ?? "Delete failed.");
}
}
}