docs(secured-writes): design — tag selector + data-type auto-fill from MxGateway browse

This commit is contained in:
Joseph Doherty
2026-06-27 12:04:12 -04:00
parent f8f01e0e5a
commit 0bbe637370
@@ -0,0 +1,171 @@
# Secured Writes — Tag Selector + Data-Type Auto-Fill (Design)
**Date:** 2026-06-27
**Component(s):** Central UI (#9), Data Connection Layer (#4); Security & Auth (#10) for the Secured Writes UX.
**Status:** Approved design — ready for implementation plan.
## Problem
On the Central UI Secured Writes page (`/operations/secured-writes`), an Operator submits a
two-person MxGateway secured write. Today the **Tag path** is a free-text input and the **Data
type** is a manually-chosen dropdown defaulting to `Boolean`. Operators must know the exact
MxGateway tag reference and the correct data type by hand — error-prone, and a wrong data type
only surfaces at *approval* time (the value can't be decoded/written).
We want:
1. The **Tag path** field to be backed by a **tag selector** (browse the connection's tags).
2. The **Data type** to be **auto-selected from the chosen tag**.
## Key insight — the data type is already available
Secured writes are **MxGateway-only** (the page filters connections to `Protocol == "MxGateway"`,
and the ManagementActor enforces the same on submit). The MxGateway gateway *already returns* a
per-attribute `data_type_name` (free-form Galaxy text, e.g. `"Float"`, `"Integer"`, `"Boolean"`)
on its browse response (`GalaxyAttribute.data_type_name` in `galaxy_repository.proto`). ScadaBridge
currently **discards** it: `RealMxGatewayClient.BrowseChildrenAsync` reads only
`FullTagReference` + `AttributeName` when building `MxBrowseChild`.
So the feature is mostly *plumbing an existing value through* + reusing the existing
`NodeBrowserDialog`. No new gRPC, no DB/schema/migration, no new cross-cluster message contracts.
Reused as-is:
- `NodeBrowserDialog` / `TreeRow` tag browser (already used by `InstanceConfigure`,
`TestBindingsDialog`).
- The full browse call path: `IBrowseService``CommunicationService.BrowseNodeAsync`
`SiteCommunicationActor``DeploymentManager``DataConnectionManagerActor`
`DataConnectionActor` → adapter.
- `BrowseNode` already carries an optional `DataType` field.
## UX decisions (confirmed)
- **Tag field:** editable text field **+ a "Browse…" button** (hybrid, matching
`InstanceConfigure`). Operators can still paste a known reference; the browser populates the field.
- **Data type:** **auto-filled from the tag but left editable**. The Galaxy type name is free-form;
the map to ScadaBridge's `DataType` enum is best-effort, so an unmapped type must leave the
operator able to choose.
## Design
### Layer 1 — Surface `DataType` from MxGateway browse (DCL)
- `Adapters/IMxGatewayClient.cs`: extend the seam record additively —
`record MxBrowseChild(string NodeId, string DisplayName, BrowseNodeClass NodeClass, bool HasChildren, string? DataType = null)`.
- `Adapters/RealMxGatewayClient.cs` (`BrowseChildrenAsync`): when projecting
`parentObject.Attributes`, pass `attr.DataTypeName` (normalise empty → `null`) into `MxBrowseChild`.
- `Adapters/MxGatewayDataConnection.cs`: forward it —
`new BrowseNode(c.NodeId, c.DisplayName, c.NodeClass, c.HasChildren, DataType: c.DataType)`.
- Test fake `FakeMxGatewayClient`: update to the new record shape and emit a sample `DataType` so
tests can assert end-to-end surfacing.
`ValueRank`/`Writable` remain `null` for MxGateway (not needed here). Array attributes
(`is_array`/`array_dimension`) are **explicitly out of scope** — a secured write to an array via
this form is an edge case; the operator can pick `List` manually.
### Layer 2 — Let the browser emit the full node (shared dialog)
`NodeBrowserDialog` currently emits only `OnSelected(string nodeId)`. Add, additively:
- `EventCallback<BrowseNode> OnNodeSelected` — invoked alongside the existing `OnSelected` in
`SelectBrowseNode()`. The internal tree node retains its source `BrowseNode` so the emit is
faithful (NodeId + DataType). Existing callers are unaffected (the callback is optional).
- `bool ShowSearch = true` — set `false` from Secured Writes. MxGateway does **not** implement
`IAddressSpaceSearchable`, so the search box would always return a `NotBrowsable` failure;
hiding it avoids a guaranteed-broken affordance. Default `true` keeps OPC UA callers unchanged.
### Layer 3 — Wire the form (`Components/Pages/Operations/SecuredWrites.razor`)
- Tag path becomes an input-group: the existing editable `<input>` + a `Browse…` button,
`disabled` unless a site **and** an MxGateway connection are selected
(`CanBrowse`, mirroring `InstanceConfigure`'s `canBrowse`).
- Render one `<NodeBrowserDialog @ref="_browserRef" SiteId="@_formSiteIdentifier"
ConnectionName="@_formConnectionName" ShowSearch="false" OnNodeSelected="OnTagPicked" />`.
- `OpenBrowser()` → `await _browserRef.ShowAsync(_formSiteIdentifier, _formConnectionName, _formTagPath)`
(seeds with whatever is already typed).
- `OnTagPicked(BrowseNode node)`:
```csharp
_formTagPath = node.NodeId;
if (SecuredWriteDataTypeMapper.TryMap(node.DataType, out var dt))
_formValueType = dt.ToString();
// else: leave _formValueType unchanged — operator picks
```
- The Data type `<select>` is unchanged (no `disabled`): pre-filled but editable. `CanSubmit`
is unchanged.
### Layer 4 — Galaxy type → `DataType` mapping
A small internal, unit-testable static class in CentralUI. Case-insensitive, whitespace-trimmed:
| Galaxy `data_type_name` | ScadaBridge `DataType` |
| --- | --- |
| `boolean`, `bool` | `Boolean` |
| `integer`, `int`, `int32` | `Int32` |
| `float` | `Float` |
| `double` | `Double` |
| `string` | `String` |
| `time` | `DateTime` |
| anything else / null / empty | *no map — operator's selection kept* |
```csharp
internal static class SecuredWriteDataTypeMapper
{
public static bool TryMap(string? galaxyTypeName, out DataType dataType)
{
dataType = default;
if (string.IsNullOrWhiteSpace(galaxyTypeName)) return false;
switch (galaxyTypeName.Trim().ToLowerInvariant())
{
case "boolean": case "bool": dataType = DataType.Boolean; return true;
case "integer": case "int": case "int32": dataType = DataType.Int32; return true;
case "float": dataType = DataType.Float; return true;
case "double": dataType = DataType.Double; return true;
case "string": dataType = DataType.String; return true;
case "time": dataType = DataType.DateTime; return true;
default: return false;
}
}
}
```
`Binary` and `List` are intentionally not auto-mapped (no clean Galaxy source); operator picks them.
## Error handling / edge cases
- Browse button disabled until site + MxGateway connection chosen.
- Gateway returns no type (older gateway / read gap) → `DataType` null → mapper returns false →
dropdown keeps its value; operator picks. Graceful.
- Unmapped Galaxy type → same graceful fallback; auto-fill stays editable so a mis-map is fixable.
- MxGateway search box hidden (`ShowSearch=false`) — tree browse + expand only.
## Testing
- **Unit:** `SecuredWriteDataTypeMapper` — every known name (incl. casing/whitespace) → expected
enum; `null` / `""` / exotic → `false`.
- **DCL:** assert a browsed MxGateway attribute's `DataTypeName` survives to `BrowseNode.DataType`
through `MxGatewayDataConnection` (via the updated `FakeMxGatewayClient`).
- **Build + targeted tests** for touched projects (DCL, DCL tests, CentralUI) — not the full suite.
- **Live-smoke:** rebuild the image (`bash docker/deploy.sh`) and exercise the page against an
MxGateway connection.
## Docs to update (spec travels with code)
- Secured Writes section under **Security & Auth** — tag selector + data-type auto-fill UX.
- MxGateway browse **type-info** note under **Data Connection Layer** — `DataType` now surfaced
for MxGateway (previously OPC-UA-only).
## Out of scope
- Array attribute handling (`is_array`) → `List`.
- OPC UA / non-MxGateway connections (Secured Writes is MxGateway-only).
- MxGateway address-space search (adapter does not implement `IAddressSpaceSearchable`).
- Any DB schema, migration, or new message contract (`ValueType` already exists on
`SubmitSecuredWriteCommand`).
## Files touched
- `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IMxGatewayClient.cs`
- `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealMxGatewayClient.cs`
- `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs`
- `FakeMxGatewayClient` (DCL test fake)
- `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/NodeBrowserDialog.razor`
- `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Operations/SecuredWrites.razor`
- new `SecuredWriteDataTypeMapper` + unit test
- Component design docs (Security & Auth, Data Connection Layer)