docs(secured-writes): design — tag selector + data-type auto-fill from MxGateway browse
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user