feat(ui): structured editors for script schemas and alarm triggers

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.
This commit is contained in:
Joseph Doherty
2026-05-13 00:33:00 -04:00
parent 57f477fd28
commit 783da8e21a
25 changed files with 3609 additions and 861 deletions

View File

@@ -0,0 +1,177 @@
# 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`](https://github.com/lovasoa/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-builder` is
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 into `wwwroot/lib/schema-editor/`. Output is
committed so `dotnet build` doesn't require Node.
- **Monaco overlap.** `jsonjoy-builder` depends on `@monaco-editor/react`,
which depends on `monaco-editor`. We already load Monaco globally for the
script code editor. The island calls `@monaco-editor/react`'s `loader.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
```ts
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 `$ref` across 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`). No `date-time` /
`format`, no `enum` / `pattern` / `min/max`, no `$ref` / `oneOf` /
`anyOf` / `allOf`, no `additionalProperties`. 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-memory `SchemaNode` / `SchemaProperty` tree 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 by `Mode="object"` (parameter list) or
`Mode="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 in `App.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.