Files
ScadaBridge/src/ScadaLink.CentralUI/Components/Shared/ParameterValueForm.razor
T
Joseph Doherty 295150751f 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.
2026-05-16 03:37:56 -04:00

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
};
}
}