1475991fd6
M1: PageErrorBoundary wraps the body of every InteractiveServer page; the inert layout boundary is removed. m1: DisposeBridge cancels its CTS when the 5 s wait times out. m2: a CSV import that commits after the modal closed still raises OnSaved. m3: document SecurityMode=None no-fallback in Client.CLI.md / Client.UI.md. m4: browsePageDelayMs written only when loaded or changed; negative flagged. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
138 lines
5.8 KiB
Plaintext
138 lines
5.8 KiB
Plaintext
@page "/scripts"
|
|
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
|
@rendermode RenderMode.InteractiveServer
|
|
@using Microsoft.EntityFrameworkCore
|
|
@using ZB.MOM.WW.OtOpcUa.Configuration
|
|
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
|
@inject IDbContextFactory<OtOpcUaConfigDbContext> DbFactory
|
|
|
|
<PageErrorBoundary>
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h4 class="mb-0">Scripts</h4>
|
|
<a href="/scripts/new" class="btn btn-primary btn-sm">New script</a>
|
|
</div>
|
|
|
|
@if (_rows is null)
|
|
{
|
|
<p>Loading…</p>
|
|
}
|
|
else
|
|
{
|
|
<section class="panel rise mt-3" style="animation-delay:.08s">
|
|
<div class="panel-head">@_rows.Count script@(_rows.Count == 1 ? "" : "s")</div>
|
|
@if (_rows.Count == 0)
|
|
{
|
|
<div style="padding:1rem" class="text-muted">No scripts defined.</div>
|
|
}
|
|
else
|
|
{
|
|
<table class="table table-sm mb-0 align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Script</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var s in _rows)
|
|
{
|
|
<tr>
|
|
<td>
|
|
<div>
|
|
<span>@(string.IsNullOrWhiteSpace(s.Name) ? s.ScriptId : s.Name)</span>
|
|
<span class="chip chip-idle ms-1">@s.Language</span>
|
|
</div>
|
|
<div class="mono small text-muted">
|
|
@s.ScriptId · hash=@DisplayText.Abbreviate(s.SourceHash, 12)
|
|
</div>
|
|
</td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
|
@onclick="() => ShowSource(s.ScriptId)">Source</button>
|
|
<a href="/scripts/@s.ScriptId" class="btn btn-sm btn-outline-primary">Edit</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
}
|
|
</section>
|
|
}
|
|
|
|
@* The dialog's EXISTENCE is gated on the held id (operator intent); the resolve drives only its
|
|
CONTENT. Gating existence on FindSourceScript() would make the modal a function of _rows, and any
|
|
render where the row is momentarily unresolvable (a future reload/poll setting _rows null or
|
|
replacing the list) would unmount the subtree and dispose the Close handler's binding id — a
|
|
click already in flight then throws GetRequiredEventBindingEntry. Latent today (_rows is written
|
|
once, in OnInitializedAsync) but the shape bit ScadaBridge live the moment a refresh landed. *@
|
|
@if (_sourceScriptId is not null)
|
|
{
|
|
var src = FindSourceScript();
|
|
<div class="modal-backdrop fade show" style="display:block"></div>
|
|
<div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
@if (src is not null)
|
|
{
|
|
@(string.IsNullOrWhiteSpace(src.Name) ? src.ScriptId : src.Name)
|
|
<span class="mono small text-muted ms-2">@src.ScriptId</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="mono">@_sourceScriptId</span>
|
|
}
|
|
</h5>
|
|
<button type="button" class="btn-close" aria-label="Close" @onclick="CloseSource"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
@if (src is not null)
|
|
{
|
|
<pre class="mono small mb-0" style="background:var(--zebra-bg);padding:1rem;border-radius:4px;overflow:auto">@src.SourceCode</pre>
|
|
}
|
|
else
|
|
{
|
|
<p class="text-muted mb-0">This script is no longer loaded.</p>
|
|
}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" @onclick="CloseSource">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</PageErrorBoundary>
|
|
|
|
@code {
|
|
private List<Script>? _rows;
|
|
|
|
// Only the id of the script whose source dialog is open — never the record itself, so a reload
|
|
// that drops or replaces the row cannot leave a stale object on screen.
|
|
private string? _sourceScriptId;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await using var db = await DbFactory.CreateDbContextAsync();
|
|
_rows = await db.Scripts.AsNoTracking()
|
|
.OrderBy(s => s.Name)
|
|
.ToListAsync();
|
|
}
|
|
|
|
/// <summary>Re-resolves the open dialog's script from the current list on every render. Drives
|
|
/// the dialog's CONTENT only — never its existence (see the comment at the markup): a script
|
|
/// that is no longer loaded renders a notice with the frame and Close still mounted.</summary>
|
|
private Script? FindSourceScript()
|
|
=> _sourceScriptId is null
|
|
? null
|
|
: _rows?.FirstOrDefault(s => s.ScriptId == _sourceScriptId);
|
|
|
|
// One dialog at a time: opening another row replaces the id.
|
|
private void ShowSource(string scriptId) => _sourceScriptId = scriptId;
|
|
|
|
private void CloseSource() => _sourceScriptId = null;
|
|
}
|