feat(adminui): typed native-alarm section in the raw tag modal (#642)
NativeAlarmModel: IsAlarm=false now removes the alarm key; advisory Warnings, driver gating, order-safe Edit. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Uns.TagEditors
|
||||
@using ZB.MOM.WW.OtOpcUa.Commons.Types
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
||||
@inject IRawTreeService Svc
|
||||
@@ -90,6 +91,9 @@
|
||||
<div class="form-text">Schemaless per driver type. Validated server-side at deploy.</div>
|
||||
}
|
||||
</div>
|
||||
<NativeAlarmEditor ConfigJson="@_form.TagConfig"
|
||||
ConfigJsonChanged="@(v => _form.TagConfig = v)"
|
||||
DriverType="@DriverType" />
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(_error))
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
@* Shared native-alarm section (Gitea #642): authors the driver-agnostic TagConfig `alarm` object
|
||||
(alarmType / severity / historizeToAveva) for a raw tag. A thin shell over NativeAlarmModel — all rules
|
||||
live there. It sits OUTSIDE the per-driver typed editor (Galaxy has none) and writes the same ConfigJson.
|
||||
|
||||
Order-safety: every change goes through NativeAlarmModel.Edit(ConfigJson, ...), which re-parses the
|
||||
LATEST json, so it can never clobber a key the driver editor just wrote; the driver models in turn
|
||||
preserve the `alarm` key (pinned by NativeAlarmEditorModelTests).
|
||||
|
||||
Plain input/select + @onchange (no InputNumber/EditForm binding — see the AdminUI binding pitfalls). *@
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors
|
||||
|
||||
@if (_m.IsAlarm || _supported)
|
||||
{
|
||||
<div class="mb-3 border rounded p-2" data-testid="native-alarm-editor">
|
||||
@if (!_supported)
|
||||
{
|
||||
@* Never hide an authored alarm silently: show it and offer removal rather than dropping it. *@
|
||||
<div class="text-warning small">
|
||||
This tag carries an <span class="mono">alarm</span> object, but driver type
|
||||
<span class="mono">@DriverType</span> does not raise native alarms, so it has no effect.
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" id="native-alarm-remove"
|
||||
@onclick="@(() => Update(m => m.IsAlarm = false))">Remove alarm object</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="form-check form-switch">
|
||||
<input id="native-alarm-enabled" type="checkbox" class="form-check-input"
|
||||
checked="@_m.IsAlarm"
|
||||
@onchange="@(e => Update(m => m.IsAlarm = e.Value is true))" />
|
||||
<label class="form-check-label" for="native-alarm-enabled">Native alarm (Part 9 condition)</label>
|
||||
</div>
|
||||
@if (_m.IsAlarm)
|
||||
{
|
||||
<div class="row g-2 mt-1">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="native-alarm-type">Alarm type</label>
|
||||
<select id="native-alarm-type" class="form-select form-select-sm" value="@_m.AlarmType"
|
||||
@onchange="@(e => Update(m => m.AlarmType = e.Value?.ToString() ?? "AlarmCondition"))">
|
||||
@foreach (var t in NativeAlarmModel.KnownAlarmTypes) { <option value="@t">@t</option> }
|
||||
@if (!NativeAlarmModel.KnownAlarmTypes.Contains(_m.AlarmType))
|
||||
{
|
||||
<option value="@_m.AlarmType">@_m.AlarmType (custom)</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="native-alarm-severity">Severity (1..1000)</label>
|
||||
<input id="native-alarm-severity" type="number" min="1" max="1000" class="form-control form-control-sm"
|
||||
value="@_m.Severity"
|
||||
@onchange="@(e => Update(m => m.Severity = int.TryParse(e.Value?.ToString(), out var s) ? s : 500))" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="native-alarm-historize">Historize to AVEVA</label>
|
||||
<select id="native-alarm-historize" class="form-select form-select-sm"
|
||||
value="@HistorizeValue(_m.HistorizeToAveva)"
|
||||
@onchange="@(e => Update(m => m.HistorizeToAveva = ParseHistorize(e.Value?.ToString())))">
|
||||
<option value="default">Default (yes)</option>
|
||||
<option value="yes">Yes</option>
|
||||
<option value="no">No (live alerts only)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@foreach (var w in _m.Warnings())
|
||||
{
|
||||
<div class="text-warning small mt-1">@w</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>The tag's raw TagConfig JSON (supports <c>@bind-ConfigJson</c>).</summary>
|
||||
[Parameter] public string? ConfigJson { get; set; }
|
||||
/// <summary>Two-way config callback.</summary>
|
||||
[Parameter] public EventCallback<string> ConfigJsonChanged { get; set; }
|
||||
/// <summary>Driver type of the owning device; gates whether the editor is offered.</summary>
|
||||
[Parameter] public string? DriverType { get; set; }
|
||||
|
||||
private NativeAlarmModel _m = new();
|
||||
private bool _supported;
|
||||
private string? _lastConfigJson;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
_supported = NativeAlarmModel.SupportsNativeAlarms(DriverType);
|
||||
if (ConfigJson == _lastConfigJson) { return; }
|
||||
_lastConfigJson = ConfigJson;
|
||||
_m = NativeAlarmModel.FromJson(ConfigJson);
|
||||
}
|
||||
|
||||
private static string HistorizeValue(bool? v) => v is null ? "default" : v.Value ? "yes" : "no";
|
||||
|
||||
private static bool? ParseHistorize(string? v) => v switch { "yes" => true, "no" => false, _ => null };
|
||||
|
||||
private async Task Update(Action<NativeAlarmModel> apply)
|
||||
{
|
||||
var json = NativeAlarmModel.Edit(ConfigJson, apply);
|
||||
_lastConfigJson = json;
|
||||
_m = NativeAlarmModel.FromJson(json);
|
||||
await ConfigJsonChanged.InvokeAsync(json);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user