Replace raw-JSON text inputs with rich UI: script parameter/return types use a JSON Schema builder (SchemaBuilder + JsonSchemaShapeParser, with a migration to convert existing definitions); alarm trigger config uses a type-aware editor with a flattened attribute picker (AlarmTriggerEditor). AlarmActor gains optional direction (rising/falling/either) on RateOfChange triggers.
7.8 KiB
Script parameter / return: JSON Schema + JSONJoy editor
Date: 2026-05-12 Status: Superseded — see "Reversal: native Blazor SchemaBuilder" below.
Decision
Replace the custom ParameterListEditor / ReturnTypeEditor Blazor components
with jsonjoy-builder (SchemaVisualEditor),
embedded as a React island. The on-disk format for TemplateScript.ParameterDefinitions
and TemplateScript.ReturnDefinition changes from the project-local flat shape
([{name,type,required,itemType?}] / {type,itemType?}) to standard JSON Schema.
Rationale
The existing flat shape lacked descriptions, defaults, enums, nested objects,
and arrays of structured items. JSON Schema covers all of that, is the
industry vocabulary other tooling already speaks (OpenAPI 3.1, function-calling
APIs, validators), and jsonjoy-builder is a polished pre-built visual editor
for it.
Trade-offs
- Breaks the no-UI-framework rule for this feature.
jsonjoy-builderis React 19 + Radix UI + Tailwind. Accepted: the island is isolated to one modal panel, Tailwind is shipped pre-built (no toolchain shared with the Blazor side), and the visual delta is contained. - New build pipeline. A small Vite project under
src/ScadaLink.CentralUI/Schema.Editor/builds a single IIFE bundle intowwwroot/lib/schema-editor/. Output is committed sodotnet builddoesn't require Node. - Monaco overlap.
jsonjoy-builderdepends on@monaco-editor/react, which depends onmonaco-editor. We already load Monaco globally for the script code editor. The island calls@monaco-editor/react'sloader.config({ monaco: window.monaco })at boot to reuse the same instance — no duplicate Monaco download.
Storage format change
| Field | Before | After |
|---|---|---|
ParameterDefinitions |
[{name,type,required,itemType?}] |
{"type":"object","properties":{...},"required":[...]} |
ReturnDefinition |
{type,itemType?} |
Any JSON Schema (root type describes the returned value) |
Per the chosen rollout: one-shot migration rewrites all existing rows on deploy. After the migration, the analysis pipeline reads JSON Schema only — no dual-format support code.
Type mapping (flat → JSON Schema):
| Flat type | JSON Schema |
|---|---|
Boolean |
{"type":"boolean"} |
Integer |
{"type":"integer"} |
Float |
{"type":"number"} |
String |
{"type":"string"} |
Object |
{"type":"object"} |
List of X |
{"type":"array","items":{"type":<X>}} |
required: false ⇒ name omitted from the required array.
required: true (default) ⇒ name added to required.
Component layout
src/ScadaLink.CentralUI/Schema.Editor/ ← new Vite project (committed)
package.json
vite.config.ts
tsconfig.json
src/main.tsx ← exposes window.ScadaSchemaEditor
src/SchemaEditorApp.tsx
src/index.css
.gitignore ← node_modules only
dist/ ← (Vite outputs to wwwroot, not here)
src/ScadaLink.CentralUI/wwwroot/lib/schema-editor/
schema-editor.js ← built IIFE, committed
schema-editor.css
src/ScadaLink.CentralUI/Components/Shared/
SchemaEditor.razor ← Blazor wrapper; mirrors MonacoEditor.razor
src/ScadaLink.CentralUI/ScriptAnalysis/
ScriptShapeParser.cs ← rewrite to read JSON Schema
src/ScadaLink.CentralUI/Components/Shared/
ScriptParameterNames.cs ← rewrite to read JSON Schema
Removed after rollout: ParameterListEditor.razor, ReturnTypeEditor.razor.
JS interop contract
window.ScadaSchemaEditor = {
mount(id: string, host: HTMLElement, options: {
value: string; // current schema JSON (may be empty)
mode: 'parameters' | 'return';
readOnly?: boolean;
}, dotNetRef: { invokeMethodAsync(name: 'OnValueChanged', json: string): Promise<void> }): void;
setValue(id: string, value: string): void;
dispose(id: string): void;
}
Migration
EF Core migration in ScadaLink.ConfigurationDatabase reads
TemplateScripts.ParameterDefinitions and ReturnDefinition from every row,
sniffs format (array vs object), translates if legacy, writes back. Idempotent:
re-running a row already in JSON Schema is a no-op. Runs once at deploy via
the existing auto-apply path.
Out of scope (deferred)
- Schema-driven value-entry forms (e.g. Inbound API tester) — would also use
jsonjoy-builder's value-editor mode, but no caller surface needs it today. - Hover/completion enhancements derived from JSON Schema descriptions or defaults. Today's pipeline only needs name + type + required.
- Reuse of JSON Schema
$refacross templates — could be a future template-level schema library.
Reversal: native Blazor SchemaBuilder (2026-05-12, same day)
JSONJoy worked but felt heavy for the actual data we author here. Specifically:
- The "Add Field" modal flow is two clicks per parameter where the legacy inline-row editor was zero. For the common 1-3 scalar-param case, a visible modal dialog every time is friction.
- JSONJoy's value-mode UX is awkward — it always renders an "Add Field" button
even when the schema's root type is
string/integer/ etc., so the Return-type tab is mismatched to the underlying single-value model. - React 19 + Radix + Tailwind for one form field is a lot of build pipeline surface to maintain.
Decision: replace JSONJoy with a Bootstrap-only Blazor component
(SchemaBuilder.razor) that recurses through its own render methods.
Storage format unchanged — still JSON Schema. The migration, parser, and
downstream analysis code are untouched.
Scope decisions (from refinement session):
- Type set: only the six JSON Schema primitives
(
string · integer · number · boolean · object · array). Nodate-time/format, noenum/pattern/min/max, no$ref/oneOf/anyOf/allOf, noadditionalProperties. Power-user expansion can come later behind a per-row "more options" toggle. - No description support per property. The row stays a single horizontal line: name + type + (items: type if array) + required + remove.
- Nested objects and arrays-of-objects recurse — same editor renders at any depth.
Files added:
src/ScadaLink.CentralUI/Components/Shared/SchemaBuilderModel.cs— in-memorySchemaNode/SchemaPropertytree plus pure-static parse / serialize. Round-trips through the canonical JSON Schema text and tolerates legacy flat-array shape as a parse fallback.src/ScadaLink.CentralUI/Components/Shared/SchemaBuilder.razor— recursive renderer driven byMode="object"(parameter list) orMode="value"(single value, with object/array falling back to the property editor).tests/ScadaLink.CentralUI.Tests/Shared/SchemaBuilderModelTests.cs— parse / serialize / round-trip / legacy-array coverage.
Files removed:
src/ScadaLink.CentralUI/Schema.Editor/(Vite project, node_modules, etc.)src/ScadaLink.CentralUI/wwwroot/lib/schema-editor/(built bundle)src/ScadaLink.CentralUI/Components/Shared/SchemaEditor.razor(Blazor wrapper)<script>/<link>references to schema-editor inApp.razor<DefaultItemExcludes>Schema.Editor/**from CentralUI csproj
Forms updated: TemplateEdit.razor, SharedScriptForm.razor,
ApiMethodForm.razor now use <SchemaBuilder> directly.
The original jsonjoy-builder integration sections above are kept for
historical context but no longer reflect what's in the codebase.