295150751f
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.
181 lines
6.5 KiB
Plaintext
181 lines
6.5 KiB
Plaintext
@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
|
|
};
|
|
}
|
|
}
|