feat(scripts): realign Test Run with runtime API, add anonymous-object calls and instance binding
The Test Run sandbox and Monaco analysis modelled a script API that had drifted from the site runtime's ScriptGlobals, so real scripts failed to compile in Test Run. Realign both to the runtime surface (Instance/Scripts/ExternalSystem/Attributes/Children/Parent) and drop the duplicate ScriptHost stub so the two cannot diverge again. - Script calls (Scripts.CallShared, Instance.CallScript, Route.To().Call) accept an anonymous object instead of a hand-built dictionary, via a shared ScriptArgs normalizer; existing dictionary calls still compile. - Test Run can optionally bind to a deployed instance, so Instance/ Attributes/CallScript route to it cross-site; adds site-side RouteToGetAttributes/RouteToSetAttributes handlers. - Adds Test Run panels to the API method and template script editors. - Fixes the TestDatabaseQuery seed script, which queried a table that never existed. Also commits unrelated in-progress work already in the tree: the health monitoring report loop, site streaming changes, and the Admin/Design data-connection and SMTP page reorganization.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
@page "/design/templates/{Id:int}"
|
||||
@using ScadaLink.Security
|
||||
@using ScadaLink.Commons.Entities.Instances
|
||||
@using ScadaLink.Commons.Entities.Templates
|
||||
@using ScadaLink.Commons.Interfaces.Repositories
|
||||
@using ScadaLink.Commons.Types.Enums
|
||||
@@ -8,7 +9,9 @@
|
||||
@using ScadaLink.TemplateEngine.Validation
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
|
||||
@inject ITemplateEngineRepository TemplateEngineRepository
|
||||
@inject ICentralUiRepository CentralUiRepository
|
||||
@inject TemplateService TemplateService
|
||||
@inject ScadaLink.CentralUI.ScriptAnalysis.ScriptAnalysisService AnalysisService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IDialogService Dialog
|
||||
@@ -106,6 +109,15 @@
|
||||
private IReadOnlyList<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext> _editorChildren
|
||||
= Array.Empty<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>();
|
||||
|
||||
// Script modal Test Run state.
|
||||
private bool _showScriptTestRun;
|
||||
private bool _scriptRunning;
|
||||
private Dictionary<string, object?> _scriptParamValues = new();
|
||||
private ScadaLink.CentralUI.ScriptAnalysis.SandboxRunResult? _scriptRunResult;
|
||||
private CancellationTokenSource? _scriptRunCts;
|
||||
private List<Instance> _deployedInstances = new();
|
||||
private string _scriptBindInstance = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Editor's Parent.* context. Empty for base templates (no owner exists);
|
||||
/// exactly one entry for derived templates — the slot-owner resolved from
|
||||
@@ -185,6 +197,13 @@
|
||||
_editorChildren = await BuildChildContextsAsync(_compositions);
|
||||
_editorParents = await BuildParentContextsAsync(Id);
|
||||
|
||||
// Deployed, running instances of this template — selectable as the
|
||||
// bind target for a script Test Run.
|
||||
_deployedInstances = (await CentralUiRepository.GetInstancesFilteredAsync(templateId: Id))
|
||||
.Where(i => i.State == InstanceState.Enabled)
|
||||
.OrderBy(i => i.UniqueName)
|
||||
.ToList();
|
||||
|
||||
_validationResult = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -926,8 +945,117 @@
|
||||
{
|
||||
<div class="text-danger small mt-2">@_scriptFormError</div>
|
||||
}
|
||||
|
||||
@if (_showScriptTestRun)
|
||||
{
|
||||
<div class="card mt-3" id="script-test-run-panel">
|
||||
<div class="card-header py-2">
|
||||
<span class="fw-semibold">Test Run <span class="badge bg-warning text-dark ms-1">Real I/O</span></span>
|
||||
</div>
|
||||
<div class="alert alert-warning py-1 mb-0 small rounded-0 border-0 border-bottom">
|
||||
<strong>Heads up:</strong>
|
||||
runs the script as typed (unsaved edits included) against the supplied
|
||||
<code>Parameters</code>.
|
||||
<code>External</code>, <code>Database</code>, and <code>Notify</code> calls fire for real against central's configured systems — real HTTP, real SQL, real emails. Side effects are permanent.
|
||||
<code>CallShared</code> executes the named shared script (saved version) in the same sandbox.
|
||||
<code>Instance</code>, <code>Attributes</code>, <code>Children</code>, <code>Parent</code>, and <code>CallScript</code> throw unless a bound instance is selected below — then they route to that live instance (attribute writes are permanent too).
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Bind to instance <span class="text-muted">(optional)</span></label>
|
||||
@if (_deployedInstances.Count == 0)
|
||||
{
|
||||
<div class="form-text">
|
||||
No running instances of this template.
|
||||
<code>Instance</code>/<code>Attributes</code>/<code>CallScript</code> will throw.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<select class="form-select form-select-sm" @bind="_scriptBindInstance">
|
||||
<option value="">— None (Instance/Attributes throw) —</option>
|
||||
@foreach (var inst in _deployedInstances)
|
||||
{
|
||||
<option value="@inst.UniqueName">@inst.UniqueName</option>
|
||||
}
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Routes <code>Instance.GetAttribute/SetAttribute</code>,
|
||||
<code>Attributes</code>, <code>Children</code>, <code>Parent</code>, and
|
||||
<code>CallScript</code> to the selected live instance.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Parameter values</label>
|
||||
<ParameterValueForm ParameterDefinitions="@_scriptParameters"
|
||||
Values="_scriptParamValues"
|
||||
ValuesChanged="@(v => _scriptParamValues = v)" />
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center mb-3">
|
||||
<button class="btn btn-primary btn-sm" @onclick="RunScriptInSandboxAsync" disabled="@_scriptRunning">
|
||||
@if (_scriptRunning)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
<span>Running…</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Run</span>
|
||||
}
|
||||
</button>
|
||||
@if (_scriptRunResult != null)
|
||||
{
|
||||
<span class="text-muted small">@_scriptRunResult.DurationMs ms</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (_scriptRunResult != null)
|
||||
{
|
||||
@if (_scriptRunResult.Success)
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-success mb-1">
|
||||
Return value <span class="badge bg-light text-dark ms-1">@_scriptRunResult.ReturnTypeName</span>
|
||||
</label>
|
||||
<pre class="bg-light border rounded p-2 small mb-0 font-monospace" style="white-space: pre-wrap;">@_scriptRunResult.ReturnValueJson</pre>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-danger mb-1">
|
||||
<span class="badge bg-danger me-1">@ScriptErrorKindLabel(_scriptRunResult.ErrorKind)</span>
|
||||
</label>
|
||||
<pre class="border border-danger-subtle rounded p-2 small mb-0 font-monospace text-danger" style="white-space: pre-wrap;">@_scriptRunResult.Error</pre>
|
||||
@if (_scriptRunResult.Markers is { Count: > 0 })
|
||||
{
|
||||
<ul class="small text-danger mt-2 mb-0">
|
||||
@foreach (var m in _scriptRunResult.Markers)
|
||||
{
|
||||
<li>Line @m.StartLineNumber, col @m.StartColumn: @m.Message <code class="ms-1">@m.Code</code></li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(_scriptRunResult.ConsoleOutput))
|
||||
{
|
||||
<div class="mb-0">
|
||||
<label class="form-label small mb-1">Console output</label>
|
||||
<pre class="bg-dark text-light rounded p-2 small mb-0 font-monospace" style="white-space: pre-wrap;">@_scriptRunResult.ConsoleOutput</pre>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline-primary btn-sm me-auto" @onclick="ToggleScriptTestRunPanel">
|
||||
@(_showScriptTestRun ? "Hide Test Run" : "Test Run")
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelScriptForm">Cancel</button>
|
||||
<button class="btn btn-success btn-sm" @onclick="SaveScript">@(editingScript ? "Save" : "Add")</button>
|
||||
</div>
|
||||
@@ -1341,6 +1469,7 @@
|
||||
_scriptReturn = null;
|
||||
_scriptIsLocked = false;
|
||||
_scriptModalTab = "code";
|
||||
ResetScriptTestRun();
|
||||
}
|
||||
|
||||
private void BeginEditScript(TemplateScript script)
|
||||
@@ -1356,6 +1485,7 @@
|
||||
_scriptReturn = script.ReturnDefinition;
|
||||
_scriptIsLocked = script.IsLocked;
|
||||
_scriptModalTab = "code";
|
||||
ResetScriptTestRun();
|
||||
}
|
||||
|
||||
private void CancelScriptForm()
|
||||
@@ -1363,8 +1493,69 @@
|
||||
_showScriptForm = false;
|
||||
_editScriptId = null;
|
||||
_scriptFormError = null;
|
||||
ResetScriptTestRun();
|
||||
}
|
||||
|
||||
private void ResetScriptTestRun()
|
||||
{
|
||||
_showScriptTestRun = false;
|
||||
_scriptRunning = false;
|
||||
_scriptParamValues = new();
|
||||
_scriptBindInstance = string.Empty;
|
||||
_scriptRunResult = null;
|
||||
_scriptRunCts?.Cancel();
|
||||
_scriptRunCts = null;
|
||||
}
|
||||
|
||||
private void ToggleScriptTestRunPanel() => _showScriptTestRun = !_showScriptTestRun;
|
||||
|
||||
private async Task RunScriptInSandboxAsync()
|
||||
{
|
||||
_scriptRunCts?.Cancel();
|
||||
_scriptRunCts = new CancellationTokenSource();
|
||||
_scriptRunning = true;
|
||||
_scriptRunResult = null;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var jsonParams = _scriptParamValues.ToDictionary(
|
||||
kv => kv.Key,
|
||||
kv => System.Text.Json.JsonSerializer.SerializeToElement(kv.Value));
|
||||
var request = new ScadaLink.CentralUI.ScriptAnalysis.SandboxRunRequest(
|
||||
_scriptCode, jsonParams, TimeoutSeconds: null,
|
||||
BindInstanceUniqueName: string.IsNullOrEmpty(_scriptBindInstance) ? null : _scriptBindInstance);
|
||||
_scriptRunResult = await AnalysisService.RunInSandboxAsync(request, _scriptRunCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException) { /* superseded by next Run click */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_scriptRunResult = new ScadaLink.CentralUI.ScriptAnalysis.SandboxRunResult(
|
||||
Success: false,
|
||||
ReturnValueJson: null,
|
||||
ReturnTypeName: null,
|
||||
ConsoleOutput: "",
|
||||
Error: $"Unexpected: {ex.GetType().Name}: {ex.Message}",
|
||||
ErrorKind: ScadaLink.CentralUI.ScriptAnalysis.SandboxErrorKind.RuntimeError,
|
||||
DurationMs: 0,
|
||||
Markers: null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_scriptRunning = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private static string ScriptErrorKindLabel(ScadaLink.CentralUI.ScriptAnalysis.SandboxErrorKind kind) => kind switch
|
||||
{
|
||||
ScadaLink.CentralUI.ScriptAnalysis.SandboxErrorKind.CompileError => "Compile error",
|
||||
ScadaLink.CentralUI.ScriptAnalysis.SandboxErrorKind.SandboxLimitation => "Sandbox limitation",
|
||||
ScadaLink.CentralUI.ScriptAnalysis.SandboxErrorKind.RuntimeError => "Runtime error",
|
||||
ScadaLink.CentralUI.ScriptAnalysis.SandboxErrorKind.Timeout => "Timeout",
|
||||
_ => "Error"
|
||||
};
|
||||
|
||||
private async Task SaveScript()
|
||||
{
|
||||
if (_selectedTemplate == null) return;
|
||||
|
||||
Reference in New Issue
Block a user