Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/SchemaLibrary.razor
T

258 lines
9.1 KiB
Plaintext

@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: list + create/edit (via SchemaBuilder) + delete the
reusable named JSON-Schema library entries that the {"$ref":"lib:Name"} resolver
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="page-head justify-content-between">
<h1 class="mb-0">Schema Library</h1>
@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" for="@($"{_fieldScope}-field-1")">Name</label>
<input aria-describedby="@($"{_helpScope}-0")" id="@($"{_fieldScope}-field-1")" type="text" class="form-control form-control-sm"
placeholder="e.g. Address" @bind="_formName" />
<div class="form-text" id="@($"{_helpScope}-0")">Referenced as <code>lib:@(string.IsNullOrWhiteSpace(_formName) ? "Name" : _formName.Trim())</code>.</div>
</div>
<div class="col-md-6">
<label class="form-label small" for="@($"{_fieldScope}-field-2")">Scope <span class="text-muted">(optional)</span></label>
<input id="@($"{_fieldScope}-field-2")" type="text" class="form-control form-control-sm"
placeholder="(global)" @bind="_formScope" />
</div>
</div>
<div class="form-label small">Schema</div>
<SchemaBuilder Mode="value"
Value="@_formSchemaJson"
ValueChanged="@(v => _formSchemaJson = v)" />
@if (_formError != null)
{
<div class="text-danger small mt-2">@_formError</div>
}
<div class="btn-group btn-group-sm mt-3" role="group" aria-label="Schema editor actions">
<button class="btn btn-success" @onclick="SaveSchema" disabled="@_busy">Save</button>
<button class="btn btn-outline-secondary" @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
{
<div class="table-responsive" role="region" aria-label="Schema library" tabindex="0">
<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>
@* Scope is operator free-text — clip it so a long value
can't push the Reference/Actions columns off-screen.
The title keeps the full value reachable. *@
<td>
<span class="cell-clip cell-clip-sm" title="@s.Scope">@(string.IsNullOrWhiteSpace(s.Scope) ? "—" : s.Scope)</span>
</td>
<td><code>lib:@s.Name</code></td>
<td class="text-end">
@* Row actions are disabled while the editor is open so the
row under edit (and its siblings) can't be deleted out from
under the form, and while a delete is in flight (_busy). *@
<div class="btn-group btn-group-sm" role="group" aria-label="@($"Actions for {s.Name}")">
<button class="btn btn-outline-primary"
@onclick="() => BeginEdit(s)" disabled="@(_editing || _busy)">Edit</button>
<button class="btn btn-outline-danger"
@onclick="() => DeleteSchema(s)" disabled="@(_editing || _busy)">Delete</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</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)
{
// In-flight guard: an editor-open row action is already disabled in the markup,
// but the _busy gate is the authoritative guard against a double-invoked delete
// (and mirrors the Save path's guard).
if (_busy) return;
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;
_busy = true;
try
{
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.");
}
}
finally
{
_busy = false;
}
}
}
@code { private readonly string _fieldScope = "field-" + Guid.NewGuid().ToString("N"); }
@code { private readonly string _helpScope = "help-" + Guid.NewGuid().ToString("N"); }