feat(mqtt): typed tag editor + validator (plain mode)
Thin typed model over a preserved JsonObject key bag, mirroring the sibling <Driver>TagConfigModel template. Key names and strictness rules mirror MqttTagDefinitionFactory rather than inventing an editor-side schema; enums round-trip as NAMES (the factory reads them strictly by name). No FullName key: under v3 a tag is identified by its RawPath, which the factory keys the definition's Name off — a composed identity key here would be dead weight nothing reads. Mode is a UI-only sub-shape selector inferred from the blob (any Sparkplug descriptor key present) and never serialised — the driver takes Plain vs SparkplugB from its DRIVER config. Only Plain Validate() is implemented; the Sparkplug branch is a stub Task 24 fills, and its keys are preserved untouched. Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
+141
@@ -0,0 +1,141 @@
|
||||
@* Typed TagConfig editor for the Mqtt driver. Same (ConfigJson/ConfigJsonChanged/DriverType/
|
||||
GetDriverConfigJson) parameter shape every typed editor takes; the last two are accepted for
|
||||
dispatch uniformity and unused here (MQTT has no address-builder picker — topics are discovered
|
||||
through the /raw browse tree, not composed from parts).
|
||||
|
||||
P1 authors Plain-mode tags only. The shape switch is real (it drives which field group renders and
|
||||
what Validate() applies) but it is a UI-only choice derived from the blob, never serialised — the
|
||||
driver takes Plain vs SparkplugB from its DRIVER config, and the tag blob has no 'mode' key.
|
||||
Task 24 replaces the Sparkplug placeholder below with the real descriptor field group. *@
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors
|
||||
@using ZB.MOM.WW.OtOpcUa.Core.Abstractions
|
||||
@using ZB.MOM.WW.OtOpcUa.Driver.Mqtt
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="mqtt-mode">Tag shape</label>
|
||||
<select id="mqtt-mode" class="form-select form-select-sm" value="@_m.Mode"
|
||||
@onchange="@(e => Update(() => _m.Mode = ParseEnum(e.Value, MqttMode.Plain)))">
|
||||
@foreach (var v in Enum.GetValues<MqttMode>()) { <option value="@v">@v</option> }
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (_m.Mode == MqttMode.Plain)
|
||||
{
|
||||
<div class="col-md-8">
|
||||
<label class="form-label" for="mqtt-topic">Topic</label>
|
||||
<input id="mqtt-topic" type="text" class="form-control form-control-sm mono"
|
||||
placeholder="factory/oven/temp" value="@_m.Topic"
|
||||
@onchange="@(e => Update(() => _m.Topic = e.Value?.ToString() ?? ""))" />
|
||||
<div class="form-text">Concrete topic — MQTT wildcards (+ / #) are not valid for a tag.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="mqtt-payload-format">Payload format</label>
|
||||
<select id="mqtt-payload-format" class="form-select form-select-sm" value="@_m.PayloadFormat"
|
||||
@onchange="@(e => Update(() => _m.PayloadFormat = ParseEnum(e.Value, MqttPayloadFormat.Json)))">
|
||||
@foreach (var v in Enum.GetValues<MqttPayloadFormat>()) { <option value="@v">@v</option> }
|
||||
</select>
|
||||
</div>
|
||||
@if (_m.PayloadFormat == MqttPayloadFormat.Json)
|
||||
{
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="mqtt-json-path">JSON path</label>
|
||||
<input id="mqtt-json-path" type="text" class="form-control form-control-sm mono"
|
||||
placeholder="$.value" value="@_m.JsonPath"
|
||||
@onchange="@(e => Update(() => _m.JsonPath = e.Value?.ToString() ?? ""))" />
|
||||
<div class="form-text">Use <code>$</code> for the whole document.</div>
|
||||
</div>
|
||||
}
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="mqtt-data-type">Data type</label>
|
||||
@* The driver's own DriverDataType members — authoring anything outside this set produces a
|
||||
blob the driver's strict parser rejects at deploy (there is no 'Double'; it is Float64). *@
|
||||
<select id="mqtt-data-type" class="form-select form-select-sm" value="@_m.DataType"
|
||||
@onchange="@(e => Update(() => _m.DataType = ParseEnum(e.Value, DriverDataType.String)))">
|
||||
@foreach (var v in Enum.GetValues<DriverDataType>()) { <option value="@v">@v</option> }
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="mqtt-qos">QoS</label>
|
||||
<select id="mqtt-qos" class="form-select form-select-sm" value="@(_m.Qos?.ToString() ?? "")"
|
||||
@onchange="@(e => Update(() => _m.Qos = ParseNullableInt(e.Value)))">
|
||||
<option value="">(driver default)</option>
|
||||
<option value="0">0 — at most once</option>
|
||||
<option value="1">1 — at least once</option>
|
||||
<option value="2">2 — exactly once</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="mqtt-retain-seed">Retained-message seed</label>
|
||||
<select id="mqtt-retain-seed" class="form-select form-select-sm"
|
||||
value="@(_m.RetainSeed is null ? "" : _m.RetainSeed.Value ? "true" : "false")"
|
||||
@onchange="@(e => Update(() => _m.RetainSeed = ParseNullableBool(e.Value)))">
|
||||
<option value="">(driver default)</option>
|
||||
<option value="true">Seed from retained message</option>
|
||||
<option value="false">Wait for a live publish</option>
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info py-2 px-3 mb-0 small" role="alert">
|
||||
Sparkplug B tag authoring is not available yet — the group / edge-node / device /
|
||||
metric fields land with Sparkplug ingest. Switch back to <strong>Plain</strong> to author
|
||||
a topic-bound tag. Any existing Sparkplug keys on this tag are preserved untouched.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_validationError is not null)
|
||||
{
|
||||
<div class="col-12"><div class="text-danger small">@_validationError</div></div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>The tag's TagConfig JSON, owned by the host modal.</summary>
|
||||
[Parameter] public string? ConfigJson { get; set; }
|
||||
/// <summary>Raised with the re-serialised TagConfig JSON after every field edit.</summary>
|
||||
[Parameter] public EventCallback<string> ConfigJsonChanged { get; set; }
|
||||
/// <summary>DriverType of the owning driver — accepted for dispatch uniformity; unused here.</summary>
|
||||
[Parameter] public string DriverType { get; set; } = "";
|
||||
/// <summary>Live accessor for the owning driver's DriverConfig JSON — accepted for dispatch uniformity; unused here.</summary>
|
||||
[Parameter] public Func<string> GetDriverConfigJson { get; set; } = () => "{}";
|
||||
|
||||
private MqttTagConfigModel _m = new();
|
||||
private string? _lastConfigJson;
|
||||
private string? _validationError;
|
||||
|
||||
// Re-parse only when the incoming JSON actually changes, so an unrelated parent re-render
|
||||
// (Blazor Server live-status pushes do this) can't reset the user's in-progress edits.
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (ConfigJson == _lastConfigJson) { return; }
|
||||
_lastConfigJson = ConfigJson;
|
||||
_m = MqttTagConfigModel.FromJson(ConfigJson);
|
||||
_validationError = _m.Validate();
|
||||
}
|
||||
|
||||
// TryParse so a bad/empty change value can never throw into the Blazor circuit — it falls back.
|
||||
private static TEnum ParseEnum<TEnum>(object? v, TEnum fallback) where TEnum : struct, Enum
|
||||
=> Enum.TryParse<TEnum>(v?.ToString(), out var r) ? r : fallback;
|
||||
|
||||
// "" ⇒ null ⇒ the key is omitted and the driver's own default applies.
|
||||
private static int? ParseNullableInt(object? v)
|
||||
=> int.TryParse(v?.ToString(), out var i) ? i : null;
|
||||
|
||||
private static bool? ParseNullableBool(object? v)
|
||||
=> bool.TryParse(v?.ToString(), out var b) ? b : null;
|
||||
|
||||
private async Task Update(Action apply)
|
||||
{
|
||||
apply();
|
||||
_validationError = _m.Validate();
|
||||
var json = _m.ToJson();
|
||||
// Keep the guard in OnParametersSet in step with what we just emitted, so the host echoing the
|
||||
// new JSON back as a parameter cannot re-parse and clobber an in-progress edit.
|
||||
_lastConfigJson = json;
|
||||
await ConfigJsonChanged.InvokeAsync(json);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user