4bb77cf0f7
Claude-Session: https://claude.ai/code/session_01G2GHPnbbKGvyyfbr2XBRVH
121 lines
8.0 KiB
Markdown
121 lines
8.0 KiB
Markdown
# Create Instance — bind connections at creation (design)
|
|
|
|
**Date:** 2026-08-21 · **Status:** **implemented** 2026-08-21 — plan `docs/plans/2026-08-21-create-instance-bindings.md`
|
|
**Mockup:** Claude artifact "Create Instance Bindings" (https://claude.ai/code/artifact/125dc749-c923-4977-a02a-eeac826e6f26)
|
|
**Builds on:** `docs/plans/2026-08-20-grouped-connection-bindings-design.md` (Configure page groups, `BindingGroupBuilder`, per-module Object apply)
|
|
|
|
## 1. Problem
|
|
|
|
`/deployment/instances/create` stops at Name / Template / Site / Area. The operator then has to
|
|
find the new instance in Topology, open Configure, and bind every module group before the first
|
|
Deploy — for a composed template like the MES Line Controller that is 8 groups / 270 rows, and
|
|
the most common question at creation is exactly "which MxAccess object (or OPC UA node) does this
|
|
instance hang off?". The answer should be given at creation, in the same shape the Configure page
|
|
now uses.
|
|
|
|
## 2. Decisions (made 2026-08-21)
|
|
|
|
| Decision | Choice | Alternatives considered |
|
|
|---|---|---|
|
|
| Shape | **Inline bindings card** on the create page — one row per module group (connection, Object, Browse…), with a "Bind all to" root picker; per-member editing stays on Configure. | Two-step wizard with the full grouped table (more screen, duplicates Configure); create-then-redirect to Configure with a banner (no new UI, but still two places). |
|
|
| What Browse… picks | **The object node.** Selecting an `Object` node writes its id into the group's Object box; members become `<object>.<memberTail>` via the existing `ApplyObject`. Picking a Variable is still allowed; the page strips the member tail. | Leaf-only selection + inference (works today, but non-obvious). |
|
|
| After Create | **Land on the new instance's Configure page** with a "created, N bound" notice, so Test Bindings / Deploy are one click away. | Back to Topology (today). |
|
|
|
|
## 3. Design
|
|
|
|
### 3.1 Preview flatten (DeploymentManager, additive)
|
|
|
|
The Configure page gets its rows from `IFlatteningPipeline.FlattenAndValidateAsync(instanceId)`,
|
|
which loads the instance from the DB. At creation there is no row yet, but
|
|
`FlatteningService.Flatten(Instance, chain, compositionMap, composedChains, dataConnections)` takes
|
|
an in-memory `Instance`. Add
|
|
|
|
```csharp
|
|
Task<Result<FlattenedConfiguration>> PreviewAsync(int templateId, int siteId, CancellationToken ct = default);
|
|
```
|
|
|
|
to `IFlatteningPipeline`, implemented in `FlatteningPipeline` by factoring the chain/composition
|
|
loading out of `FlattenAndValidateAsync` into a private helper and calling `Flatten` with an
|
|
unsaved `new Instance { TemplateId, SiteId, Name = "(preview)" }` (no overrides, no bindings).
|
|
No validation pass (no scripts, no binding completeness) — only the attribute list is needed.
|
|
Same `FlattenSession` memo, so it is one template-chain walk per call.
|
|
|
|
### 3.2 Create page (CentralUI `InstanceCreate.razor` + new code-behind)
|
|
|
|
- **Superseded 2026-08-21 (`docs/plans/2026-08-21-template-picker.md`):** the Template `<select>`
|
|
described below is now a read-only folder-path field + `TemplatePickerDialog`, and the page takes
|
|
a `?templateId=` query preselect (what the Templates tree's "New Instance…" navigates with). The
|
|
card's trigger is unchanged — a pick still calls `OnTemplateOrSiteChangedAsync()`.
|
|
- The **Connection Bindings** card appears once Template *and* Site are set; changing either
|
|
re-runs the preview (a pick or a site-select change — no typing involved) and resets
|
|
the card. Rows = `PreviewAsync(...).Attributes.Where(DefaultDataSourceReference ?? DataSourceReference != null)`
|
|
→ `BindingRow(canonicalName, default, null, null)` → `BindingGroupBuilder.Build(rows, siteConnectionIds)`.
|
|
- **Group rows** are the Configure page's group-header row (connection select, Object input,
|
|
Browse…, bound/unbound pill) without the member table. A third pill on the Object cell says
|
|
where the value came from: `from template` (the composed defaults already share a prefix —
|
|
`InferredObject` non-null before any edit), `suggested` (set by Prefill), nothing (typed).
|
|
- **Bind all to** = connection + root Object + Browse… + **Prefill groups**. Prefill sets the
|
|
connection on every group, puts the root on `(instance)`, and for module groups with a blank
|
|
Object whose defaults are bare member names suggests `{root}.{groupKey}`. It never overwrites
|
|
a typed Object; groups with `from template` keep theirs. The suggestion is deliberately
|
|
*not* applied silently — sibling galaxy objects (Z28061's `Left_002` vs `Z28061.Left_002`) are
|
|
common, so the operator must be able to see and correct it before Create.
|
|
- **Browse…** per group opens `NodeBrowserDialog` for that group's connection, rooted at the root
|
|
Object when set (`InitialNodeId`), with the new `SelectableNodeClasses = [Object, Variable]`.
|
|
Object pick → Object box as-is. Variable pick → if its id ends with `.<memberTail>` of any
|
|
member in the group, strip the tail; otherwise use as-is.
|
|
- **Skip bindings** checkbox collapses the card and restores today's behaviour.
|
|
- **Create instance**: validate name/template/site/scope as today; `CreateInstanceAsync`; then,
|
|
unless skipped, compute the binding list by running `ApplyObject` on every group with a
|
|
connection and a non-blank Object (rows with connection but blank Object are left unbound —
|
|
no partial `<member>`-only binding is written) and call `SetConnectionBindingsAsync` (an upsert).
|
|
Navigate to `/deployment/instances/{id}/configure?created=1&bound=N`. If the bindings call
|
|
fails **after** the instance was created, still navigate to Configure and pass the error
|
|
(`?bindError=…`, length-capped) so the operator is not left on a form for an instance that now
|
|
exists.
|
|
- State table (empty template, site with no connections, preview failure, in-flight) is in the
|
|
mockup §3; all non-fatal states keep Create enabled and create without bindings.
|
|
|
|
Pure logic lives in a new `Services/CreateBindingPlanner.cs` (Prefill, tail-stripping, binding
|
|
list assembly) so it is unit-testable without rendering, mirroring `BindingGroupBuilder`.
|
|
|
|
### 3.3 NodeBrowserDialog (CentralUI, additive)
|
|
|
|
New parameter `IReadOnlyCollection<BrowseNodeClass> SelectableNodeClasses` defaulting to
|
|
`[Variable]`, consulted by `Select`/`SelectBrowseNode`; rows of a selectable non-Variable class
|
|
get an `object` badge and the footer prompt reads "Select an object". Existing callers
|
|
(Configure, template editors) are unchanged.
|
|
|
|
### 3.4 Configure page
|
|
|
|
Reads `created`/`bound`/`bindError` query parameters and shows a one-shot notice above the
|
|
Connection Bindings panel. Nothing else changes.
|
|
|
|
### 3.5 CLI
|
|
|
|
No new command: `instance create` followed by `instance bind-module` / `set-bindings` already
|
|
covers the same operation. The CLI README gets a one-line cross-reference from the create
|
|
command to the binding commands.
|
|
|
|
## 4. Out of scope
|
|
|
|
- Per-member editing at creation (Configure's job).
|
|
- Making OPC UA object concatenation work for servers with opaque node ids — unchanged caveat
|
|
from the Configure design; the default reference is visible on Configure for those.
|
|
- A template-level "preferred connection" default (would save the Bind-all step) — possible
|
|
follow-up, needs a template schema change.
|
|
- Test Bindings at creation (needs a saved instance; it is one click away on Configure).
|
|
|
|
## 5. Testing
|
|
|
|
- `FlatteningPipelineTests`: `PreviewAsync` returns the same attribute set (canonical names +
|
|
default references) as `FlattenAndValidateAsync` on a freshly created, un-overridden instance of
|
|
the same template/site; unknown template/site → Failure.
|
|
- `CreateBindingPlannerTests`: Prefill (blank-only, `from template` preserved, nested module
|
|
keys), Variable-pick tail stripping, binding list excludes connection-without-Object rows,
|
|
Skip ⇒ empty list.
|
|
- `NodeBrowserDialog` bUnit test (if the project already renders it) or `TreeRow` selection
|
|
test: Object selectable only when listed.
|
|
- Live check on the docker rig with a composed template: create → lands on Configure with the
|
|
expected groups bound; Deploy gate accepts; Test Bindings reads.
|