docs: grouped connection bindings — Central UI Configure page, ResolvedAttribute.DefaultDataSourceReference, CLI instance bind-module; correct the stale 'set-bindings REPLACES all' claim (server upserts) in README + help text
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Grouped Connection Bindings — design
|
||||
|
||||
**Date:** 2026-08-20 · **Status:** approved, implementation pending · **Components:** Central UI (#15), Template Engine (flattening), CLI
|
||||
**Date:** 2026-08-20 · **Status:** implemented 2026-08-20 on `feat/grouped-connection-bindings` (plan `2026-08-20-grouped-connection-bindings.md`, 9 tasks) · **Components:** Central UI (#15), Template Engine (flattening), CLI
|
||||
|
||||
## Problem
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ scadabridge instance list [--site-id <id>] [--template-id <id>] [--search <term>
|
||||
scadabridge instance get --id <id>
|
||||
scadabridge instance create --name <name> --template-id <id> --site-id <id> [--area-id <id>]
|
||||
scadabridge instance set-bindings --id <id> --bindings <json>
|
||||
scadabridge instance bind-module --id <id> --module <name> --connection <id> --object <path> [--dry-run]
|
||||
scadabridge instance set-overrides --id <id> --overrides <json>
|
||||
scadabridge instance import-overrides --id <id> --file <path.csv>
|
||||
scadabridge instance alarm-override set --instance-id <id> --alarm <name> [--trigger-config <json>] [--priority <n>]
|
||||
|
||||
@@ -119,7 +119,11 @@ Central cluster only. Sites have no user interface.
|
||||
### Instance Management (Deployment Role)
|
||||
- Create instances from templates at a specific site.
|
||||
- Assign instances to areas.
|
||||
- Bind data connections — **per-attribute binding** where each attribute with a data source reference individually selects its data connection from the site's available connections. **Bulk assignment** supported: select multiple attributes and assign a data connection to all of them at once. Each row also exposes:
|
||||
- Bind data connections — **per-attribute binding** where each attribute with a data source reference individually selects its data connection from the site's available connections. **The binding rows come from the instance's flattened configuration** (`IFlatteningPipeline.FlattenAndValidateAsync(id, validateScripts:false)`), so every data-sourced attribute the deploy gate will check appears — composed feature-module members (`LeftMESReceiver.MoveInFlag`, `LeftReactorSide.LeakTest.DeltaVac`, …) included. (Until 2026-08-20 the panel was built from the template's *direct* attributes only, which left 264 of a composed QA instance's 280 bindings invisible and made the instance undeployable from the UI; design `docs/plans/2026-08-20-grouped-connection-bindings-design.md`.) Only a genuine flatten *failure* falls back to the direct-attribute list, with a visible warning banner; a validation failure (e.g. unbound attributes — the very thing the panel exists to fix) is not a reason to fall back.
|
||||
- **Grouped by module.** Rows are grouped by the first segment of the canonical name — `(instance)` for direct attributes first, then one collapsible group per composed module in name order; nested modules fold into their first segment (`LeftReactorSide` shows *incl. LeakTest*) and members keep their nested tail (`LeakTest.DeltaVac`). The pure grouping/inference/apply logic is `BindingGroupBuilder` (CentralUI `Services/`), unit-tested without rendering the page.
|
||||
- **Group header = connection + Object + Apply.** Each group header carries a connection select and an **Object** input; **Apply to n** rewrites every member of the group to `<object>.<memberTail>` on that connection (a blank Object clears the overrides so the template default applies; surrounding whitespace and a trailing `.` are tolerated). The Object input is pre-filled with the **inferred** shared prefix when every overridden member agrees (`MESReceiver_022.MoveInFlag` + `MESReceiver_022.ScanStateCmd` ⇒ `MESReceiver_022`; nested object paths such as `Left_002.DelmiaReceiver` infer too); when members disagree, or an override does not end in its member tail, the group shows a **mixed** pill and the input's placeholder invites an object to unify them. Apply edits the in-memory maps only — **Save Bindings** persists every row (hidden rows included) through the existing `SetConnectionBindings` upsert, and the header shows an `n changed · unsaved` pill until then.
|
||||
- **Pills and filters.** The card header totals unbound rows and mixed groups; each group shows `n unbound` (red) or `bound`, plus `mixed`; unbound rows render highlighted. A **Show only unbound** filter hides bound rows and fully-bound groups; **Expand all** opens every group. The legacy all-groups connection select (**Assign all to…**) is retained. A composed member whose template default is the bare member name (`MoveInFlag`) carries a ⚠ marker — such a default cannot be a real address and the deploy gate will reject the instance until an override (or a group Object) is set.
|
||||
- **Template default** column shows `ResolvedAttribute.DefaultDataSourceReference` — the reference as resolved from the template chain *before* the per-instance override — so the operator always sees both the default and the effective override. **Test Bindings** (button label carries the testable-row count) reads every bound row with an effective path, composed rows included.
|
||||
- **Override** — optional per-attribute OPC UA node id (or other protocol address). When set, replaces the template's `DataSourceReference` at flattening time; when blank, the template default is used. The greyed placeholder shows the template default for context.
|
||||
- **Browse…** — opens the OPC UA Tag Browser dialog, populated live from the site's OPC UA server via `BrowseOpcUaNodeCommand`. Visible only when the row's connection uses the OPC UA protocol; disabled until a connection is picked on that row. The dialog lazy-loads the address space, supports manual node-id entry as a fallback, and remains usable when the site or its OPC UA session is offline (the manual-paste field stays active even on error). The dialog adds:
|
||||
- **Load more** — when a browse level is truncated, a "Load more" affordance fetches the next page using the server's continuation point (`BrowseNext`); an expired continuation point falls back to a fresh browse.
|
||||
|
||||
@@ -292,7 +292,7 @@ Each flattened configuration output includes a **revision hash** (computed from
|
||||
- Staleness detection: comparing the deployed revision to the current template-derived revision without a full diff.
|
||||
- Diff correlation: ensuring diffs are computed against a consistent baseline.
|
||||
|
||||
The override flows into the flattened attribute's `DataSourceReference` and therefore participates in the revision hash — changes to an instance's binding overrides re-deploy as expected.
|
||||
The override flows into the flattened attribute's `DataSourceReference` and therefore participates in the revision hash — changes to an instance's binding overrides re-deploy as expected. The pre-override value is preserved alongside it as **`ResolvedAttribute.DefaultDataSourceReference`** (2026-08-20, grouped connection bindings): populated by `FlatteningService.ApplyConnectionBindings` for *every* data-sourced attribute, bound or not (equal to `DataSourceReference` when no override is set; null for non-data-sourced attributes). It is central-side presentation data for the Configure page's *Template default* column and is **deliberately excluded from the revision hash** — `RevisionHashService` projects through `HashableAttribute`, which does not carry it — so adding it changed no deployed revision (pinned by `DefaultDataSourceReferenceTests`).
|
||||
|
||||
The revision hash covers the resolved attributes, alarms, scripts, connections, **and native alarm source bindings** (`ResolvedNativeAlarmSource`: canonical name, connection name, source reference, condition filter, and locked state). Native alarm sources were folded into the hash in migration TemplateEngine-011; the hashable projection is populated null-when-empty and the canonical serializer omits null properties, so every native-source-free configuration keeps its byte-identical pre-migration hash. **Migration note:** native-source-bearing instances will report stale once after upgrade; redeploy clears it — deliberate.
|
||||
|
||||
|
||||
@@ -62,9 +62,9 @@ public static class InstanceCommands
|
||||
"[attributeName, dataConnectionId, dataSourceReferenceOverride] " +
|
||||
"(the 3rd element overrides the attribute's data-source reference; " +
|
||||
"pass null or omit it to use the template default). " +
|
||||
"NOTE: this REPLACES all bindings for the instance — include the " +
|
||||
"override on every entry that needs one, or omitting it clears any " +
|
||||
"previously-set override.",
|
||||
"NOTE: this UPSERTS — listed attributes are updated or added, unlisted " +
|
||||
"ones keep their binding; omitting the 3rd element on a listed entry " +
|
||||
"clears that attribute's previously-set override.",
|
||||
Required = true
|
||||
};
|
||||
|
||||
|
||||
@@ -683,9 +683,39 @@ scadabridge --url <url> instance set-bindings --id 42 \
|
||||
--bindings '[["Speed",7,"ns=2;s=Reactor.Speed"],["Mode",7]]'
|
||||
```
|
||||
|
||||
> **Note:** this command **replaces** all bindings for the instance. Include the
|
||||
> override on every entry that needs one — omitting the third element clears any
|
||||
> previously-set override for that attribute.
|
||||
> **Note:** this command **upserts** — each listed attribute is updated (or added);
|
||||
> attributes *not* listed keep their existing binding. Within a listed entry, omitting
|
||||
> the third element clears any previously-set override for that attribute.
|
||||
|
||||
#### `instance bind-module`
|
||||
|
||||
Rebind every member of one composed module (or the instance's direct attributes) to a
|
||||
new object path in a single command — the CLI counterpart of the Configure page's
|
||||
per-group **Apply**. Reads the instance's current bindings (`GetInstance`), rewrites the
|
||||
module's rows client-side to `<object>.<memberTail>`, and upserts only those rows
|
||||
(`SetConnectionBindings`), so every other module is left untouched.
|
||||
|
||||
```sh
|
||||
scadabridge --url <url> instance bind-module --id <int> --module <name> --connection <int> --object <path> [--dry-run]
|
||||
```
|
||||
|
||||
| Option | Required | Description |
|
||||
|--------|----------|-------------|
|
||||
| `--id` | yes | Instance ID |
|
||||
| `--module` | yes | Module key = first segment of the canonical attribute name (e.g. `RightMESReceiver`, which also covers nested `RightMESReceiver.LeakTest.*`). Use `(instance)` for the direct (dot-less) attributes |
|
||||
| `--connection` | yes | Data connection ID every member is bound to |
|
||||
| `--object` | yes | Object path the members hang off (`MESReceiver_022`, `Left_002.DelmiaReceiver`); a trailing `.` is tolerated. An **empty string clears** the overrides so the template defaults apply |
|
||||
| `--dry-run` | no | Print the resulting bindings without saving |
|
||||
|
||||
```sh
|
||||
# Repoint the right-hand MES receiver module of instance 1 to the MESReceiver_022 object.
|
||||
scadabridge --url <url> instance bind-module --id 1 --module RightMESReceiver --connection 1 --object MESReceiver_022
|
||||
```
|
||||
|
||||
The module must already have at least one binding row on the instance — the command
|
||||
rewrites existing rows, it does not discover the template's members. A module with no
|
||||
bindings yet must be seeded once with `instance set-bindings` (or bound once in the Central
|
||||
UI); an unknown module fails with `UNKNOWN_MODULE` and lists the modules that do have rows.
|
||||
|
||||
#### `instance set-overrides`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user