@namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
@using System.Globalization
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
@* Rich alarm trigger configuration editor. Replaces the raw JSON text field
used for TemplateAlarm.TriggerConfiguration. The editor emits the same JSON
shape that AlarmActor.ParseEvalConfig consumes:
ValueMatch { attributeName, matchValue } ("!=X" prefix = not equals)
RangeViolation { attributeName, min, max }
RateOfChange { attributeName, thresholdPerSecond, windowSeconds, direction } *@
@* ── Monitored attribute ───────────────────────────────────────────── *@
@* Expression triggers reference attributes inside the C# expression itself,
so they do not use the single-attribute picker. *@
@if (TriggerType != AlarmTriggerType.Expression)
{
Set any subset of the four setpoints. The most-severe matching band
wins (LoLo/HiHi outrank Lo/Hi). Per-setpoint priority overrides the
alarm-level priority for that band. Deadband (optional) relaxes the
deactivation threshold by the configured amount to prevent flapping.
@HiLoSetpointRow("HIGH-HIGH (critical)",
_hiHiText, v => _hiHiText = v, OnHiHiChanged,
_hiHiDeadbandText, v => _hiHiDeadbandText = v, OnHiHiDeadbandChanged,
_hiHiPriorityText, v => _hiHiPriorityText = v, OnHiHiPriorityChanged,
_hiHiMessageText, v => _hiHiMessageText = v, OnHiHiMessageChanged,
"text-danger", "alarm-hilo-hihi")
@HiLoSetpointRow("HIGH (warning)",
_hiText, v => _hiText = v, OnHiChanged,
_hiDeadbandText, v => _hiDeadbandText = v, OnHiDeadbandChanged,
_hiPriorityText, v => _hiPriorityText = v, OnHiPriorityChanged,
_hiMessageText, v => _hiMessageText = v, OnHiMessageChanged,
"text-warning-emphasis", "alarm-hilo-hi")
@HiLoSetpointRow("LOW (warning)",
_loText, v => _loText = v, OnLoChanged,
_loDeadbandText, v => _loDeadbandText = v, OnLoDeadbandChanged,
_loPriorityText, v => _loPriorityText = v, OnLoPriorityChanged,
_loMessageText, v => _loMessageText = v, OnLoMessageChanged,
"text-warning-emphasis", "alarm-hilo-lo")
@HiLoSetpointRow("LOW-LOW (critical)",
_loLoText, v => _loLoText = v, OnLoLoChanged,
_loLoDeadbandText, v => _loLoDeadbandText = v, OnLoLoDeadbandChanged,
_loLoPriorityText, v => _loLoPriorityText = v, OnLoLoPriorityChanged,
_loLoMessageText, v => _loLoMessageText = v, OnLoLoMessageChanged,
"text-danger", "alarm-hilo-lolo")
};
///
/// Renders one setpoint row: value (number) + priority (int). Both are
/// optional — leaving a value blank disables that band. The
/// tints the label to convey relative
/// severity at a glance.
///
private RenderFragment HiLoSetpointRow(
string label,
string? value, Action valueSetter, Func onValueChanged,
string? deadband, Action deadbandSetter, Func onDeadbandChanged,
string? priority, Action prioritySetter, Func onPriorityChanged,
string? message, Action messageSetter, Func onMessageChanged,
string severityClass, string setpointTestId) => __builder =>
{
A boolean C# expression — e.g. Attributes["Temperature"] > 80.
@* Analysis-kind selector — Advisory (default) keeps the blank-expression
finding as a non-blocking warning; Strict escalates it to a deploy-blocking error. *@
};
private async Task OnExpressionChanged(string value)
{
_model.Expression = value;
await Emit();
}
// Backing field + handler for the Advisory|Strict selector.
private string _analysisKindValue = "Advisory";
private async Task OnAnalysisKindChanged()
{
_model.IsStrictAnalysisKind = string.Equals(_analysisKindValue, "Strict", StringComparison.OrdinalIgnoreCase);
await Emit();
}
// ── Hint text ──────────────────────────────────────────────────────────
private string BuildHint()
{
var attr = string.IsNullOrWhiteSpace(_model.AttributeName)
? "the selected attribute"
: $"\"{_model.AttributeName}\"";
return TriggerType switch
{
AlarmTriggerType.ValueMatch =>
$"Triggers when {attr} {(_model.NotEquals ? "is not equal to" : "equals")} \"{_model.MatchValue ?? ""}\".",
AlarmTriggerType.RangeViolation =>
_model.Min.HasValue && _model.Max.HasValue
? $"Triggers when {attr} < {Fmt(_model.Min)} or > {Fmt(_model.Max)}."
: $"Triggers when {attr} goes outside the configured range.",
AlarmTriggerType.RateOfChange =>
$"Triggers when {attr} changes faster than {Fmt(_model.ThresholdPerSecond) ?? "?"} units/sec ({_model.Direction}) over a {Fmt(_model.WindowSeconds) ?? "?"} sec window.",
AlarmTriggerType.HiLo => BuildHiLoHint(attr),
AlarmTriggerType.Expression =>
"Alarm is active while this expression is true.",
_ => string.Empty
};
}
private string BuildHiLoHint(string attr)
{
var parts = new List();
if (_model.LoLo.HasValue) parts.Add($"LoLo at {Fmt(_model.LoLo)}");
if (_model.Lo.HasValue) parts.Add($"Lo at {Fmt(_model.Lo)}");
if (_model.Hi.HasValue) parts.Add($"Hi at {Fmt(_model.Hi)}");
if (_model.HiHi.HasValue) parts.Add($"HiHi at {Fmt(_model.HiHi)}");
if (parts.Count == 0)
return $"Triggers when {attr} crosses any configured setpoint (none set yet).";
return $"Triggers on {attr}: {string.Join(", ", parts)}.";
}
private static string Fmt(double? v) =>
v.HasValue ? v.Value.ToString("0.###", CultureInfo.InvariantCulture) : "";
private static string FormatNullable(double? v) =>
v.HasValue ? v.Value.ToString("R", CultureInfo.InvariantCulture) : "";
private static double? ParseDouble(string? s) =>
double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : null;
}