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:
@@ -27,6 +27,13 @@
|
||||
[Parameter] public bool ReadOnly { get; set; } = false;
|
||||
[Parameter] public bool ShowToolbar { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime globals surface the script is analyzed against. Defaults to
|
||||
/// template/shared-script globals; set to <c>InboundApi</c> on the API
|
||||
/// method editor so <c>Route</c> and <c>Parameters</c> type-check.
|
||||
/// </summary>
|
||||
[Parameter] public ScriptAnalysis.ScriptKind ScriptKind { get; set; } = ScriptAnalysis.ScriptKind.Template;
|
||||
|
||||
/// <summary>
|
||||
/// Parameter names declared on the form (derived from the SchemaBuilder's
|
||||
/// JSON Schema), surfaced as completions inside Parameters["..."] literals
|
||||
@@ -148,7 +155,8 @@
|
||||
?? Array.Empty<ScriptAnalysis.ParameterShape>(),
|
||||
SelfAttributes?.ToArray() ?? Array.Empty<ScriptAnalysis.AttributeShape>(),
|
||||
Children?.ToArray() ?? Array.Empty<ScriptAnalysis.CompositionContext>(),
|
||||
Parent);
|
||||
Parent,
|
||||
ScriptKind);
|
||||
|
||||
private async Task FormatAsync()
|
||||
{
|
||||
@@ -189,5 +197,6 @@
|
||||
ScriptAnalysis.ParameterShape[] DeclaredParameterShapes,
|
||||
ScriptAnalysis.AttributeShape[] SelfAttributes,
|
||||
ScriptAnalysis.CompositionContext[] Children,
|
||||
ScriptAnalysis.CompositionContext? Parent);
|
||||
ScriptAnalysis.CompositionContext? Parent,
|
||||
ScriptAnalysis.ScriptKind ScriptKind);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
@using ScadaLink.CentralUI.ScriptAnalysis
|
||||
@using System.Text.Json
|
||||
|
||||
@*
|
||||
Renders an input row per declared parameter so the user can supply values
|
||||
for a script test run. Primitive types get typed inputs (text / number /
|
||||
checkbox); Object and List fall back to a JSON textarea with inline parse
|
||||
errors. The companion SchemaBuilder edits the schema; this edits values.
|
||||
*@
|
||||
|
||||
@if (Shapes.Count == 0)
|
||||
{
|
||||
<div class="text-muted small fst-italic">No parameters declared.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="d-flex flex-column gap-2">
|
||||
@foreach (var shape in Shapes)
|
||||
{
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label small mb-0" for="@FieldId(shape)">
|
||||
<code>@shape.Name</code>
|
||||
<span class="text-muted ms-1">@shape.Type@(shape.Required ? "" : "?")</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
@RenderInput(shape)
|
||||
@if (_parseErrors.TryGetValue(shape.Name, out var err))
|
||||
{
|
||||
<div class="text-danger small mt-1">@err</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public string? ParameterDefinitions { get; set; }
|
||||
[Parameter] public Dictionary<string, object?> Values { get; set; } = new();
|
||||
[Parameter] public EventCallback<Dictionary<string, object?>> ValuesChanged { get; set; }
|
||||
|
||||
private IReadOnlyList<ParameterShape> Shapes =>
|
||||
ScriptParameterNames.ParseShapes(ParameterDefinitions);
|
||||
|
||||
private readonly Dictionary<string, string> _rawText = new();
|
||||
private readonly Dictionary<string, string> _parseErrors = new();
|
||||
|
||||
private static string FieldId(ParameterShape shape) => $"param-{shape.Name}";
|
||||
|
||||
private RenderFragment RenderInput(ParameterShape shape) => __builder =>
|
||||
{
|
||||
switch (shape.Type)
|
||||
{
|
||||
case "Boolean":
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="@FieldId(shape)"
|
||||
checked="@AsBool(shape.Name)"
|
||||
@onchange="e => SetBool(shape.Name, (bool)(e.Value ?? false))" />
|
||||
</div>
|
||||
break;
|
||||
|
||||
case "Integer":
|
||||
<input class="form-control form-control-sm" type="number" step="1" id="@FieldId(shape)"
|
||||
value="@AsRaw(shape.Name)"
|
||||
@oninput="e => SetNumeric(shape.Name, (string?)e.Value, integerOnly: true)" />
|
||||
break;
|
||||
|
||||
case "Float":
|
||||
<input class="form-control form-control-sm" type="number" step="any" id="@FieldId(shape)"
|
||||
value="@AsRaw(shape.Name)"
|
||||
@oninput="e => SetNumeric(shape.Name, (string?)e.Value, integerOnly: false)" />
|
||||
break;
|
||||
|
||||
case "String":
|
||||
<input class="form-control form-control-sm" type="text" id="@FieldId(shape)"
|
||||
value="@AsRaw(shape.Name)"
|
||||
@oninput="e => SetString(shape.Name, (string?)e.Value)" />
|
||||
break;
|
||||
|
||||
default: // Object, List, List<...>, unknown
|
||||
<textarea class="form-control form-control-sm font-monospace" rows="3" id="@FieldId(shape)"
|
||||
placeholder='@($"JSON {shape.Type.ToLowerInvariant()}")'
|
||||
@oninput="e => SetJson(shape.Name, (string?)e.Value)">@AsRaw(shape.Name)</textarea>
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private string AsRaw(string name) =>
|
||||
_rawText.TryGetValue(name, out var raw) ? raw : "";
|
||||
|
||||
private bool AsBool(string name) =>
|
||||
Values.TryGetValue(name, out var v) && v is bool b && b;
|
||||
|
||||
private async Task SetString(string name, string? raw)
|
||||
{
|
||||
_rawText[name] = raw ?? "";
|
||||
_parseErrors.Remove(name);
|
||||
Values[name] = raw ?? "";
|
||||
await ValuesChanged.InvokeAsync(Values);
|
||||
}
|
||||
|
||||
private async Task SetBool(string name, bool value)
|
||||
{
|
||||
_parseErrors.Remove(name);
|
||||
Values[name] = value;
|
||||
await ValuesChanged.InvokeAsync(Values);
|
||||
}
|
||||
|
||||
private async Task SetNumeric(string name, string? raw, bool integerOnly)
|
||||
{
|
||||
_rawText[name] = raw ?? "";
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
_parseErrors.Remove(name);
|
||||
Values.Remove(name);
|
||||
await ValuesChanged.InvokeAsync(Values);
|
||||
return;
|
||||
}
|
||||
if (integerOnly && long.TryParse(raw, out var i))
|
||||
{
|
||||
_parseErrors.Remove(name);
|
||||
Values[name] = i;
|
||||
}
|
||||
else if (!integerOnly && double.TryParse(raw,
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var d))
|
||||
{
|
||||
_parseErrors.Remove(name);
|
||||
Values[name] = d;
|
||||
}
|
||||
else
|
||||
{
|
||||
_parseErrors[name] = integerOnly ? "Not a valid integer." : "Not a valid number.";
|
||||
Values.Remove(name);
|
||||
}
|
||||
await ValuesChanged.InvokeAsync(Values);
|
||||
}
|
||||
|
||||
private async Task SetJson(string name, string? raw)
|
||||
{
|
||||
_rawText[name] = raw ?? "";
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
_parseErrors.Remove(name);
|
||||
Values.Remove(name);
|
||||
await ValuesChanged.InvokeAsync(Values);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(raw);
|
||||
Values[name] = JsonElementToObject(doc.RootElement.Clone());
|
||||
_parseErrors.Remove(name);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_parseErrors[name] = $"JSON parse error: {ex.Message}";
|
||||
Values.Remove(name);
|
||||
}
|
||||
await ValuesChanged.InvokeAsync(Values);
|
||||
}
|
||||
|
||||
private static object? JsonElementToObject(JsonElement element)
|
||||
{
|
||||
return element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => element.GetString(),
|
||||
JsonValueKind.Number => element.TryGetInt64(out var i) ? (object)i : element.GetDouble(),
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
JsonValueKind.Null => null,
|
||||
JsonValueKind.Array => element.EnumerateArray().Select(JsonElementToObject).ToList(),
|
||||
JsonValueKind.Object => element.EnumerateObject()
|
||||
.ToDictionary(p => p.Name, p => JsonElementToObject(p.Value)),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user