feat(m9/T32c): schema-library CRUD commands + handlers + Central UI page + read-accessor
This commit is contained in:
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user