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