@namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
@using System.Globalization
@* Structured editor for a template script's trigger. Owns both the trigger-type
selector and the type-specific configuration, emitting (via Changed) the
canonical TriggerType string + the TriggerConfiguration JSON that
ScriptActor.ParseTriggerConfig consumes:
Interval { intervalMs }
ValueChange { attributeName }
Conditional { attributeName, operator, threshold, mode }
Expression { expression, mode }
Call { } *@
@* ── Trigger type ──────────────────────────────────────────────────── *@
@* ── Type-specific configuration ───────────────────────────────────── *@
@switch (_kind)
{
case ScriptTriggerKind.Interval:
@RenderInterval();
break;
case ScriptTriggerKind.ValueChange:
@RenderAttributePicker("Monitored attribute")
break;
case ScriptTriggerKind.Conditional:
@RenderConditional();
break;
case ScriptTriggerKind.Expression:
@RenderExpression();
break;
case ScriptTriggerKind.Call:
No automatic trigger — this script runs only when another script
invokes it via Instance.CallScript("...").
break;
case ScriptTriggerKind.Unknown:
Unrecognized trigger type @_rawType. Its stored
configuration is shown below and left untouched — pick a known
trigger type above to reconfigure it.
break;
}
@* ── Fire mode (Conditional + Expression) ──────────────────────────── *@
@if (_kind is ScriptTriggerKind.Conditional or ScriptTriggerKind.Expression)
{
@RenderMode();
}
@* ── Hint ──────────────────────────────────────────────────────────── *@
@if (_kind is ScriptTriggerKind.Interval or ScriptTriggerKind.ValueChange
or ScriptTriggerKind.Conditional or ScriptTriggerKind.Expression)
{
@BuildHint()
}
@code {
// ── Parameters ─────────────────────────────────────────────────────────
[Parameter] public string? TriggerType { get; set; }
[Parameter] public string? TriggerConfig { get; set; }
/// Raised whenever the type or config changes — emits both atomically.
[Parameter] public EventCallback Changed { get; set; }
/// Flattened attribute list (direct + inherited + composed) for the picker.
[Parameter] public IReadOnlyList AvailableAttributes { get; set; } =
Array.Empty();
// ── Internal state ─────────────────────────────────────────────────────
private ScriptTriggerKind _kind;
private string _kindValue = "None";
private string? _rawType;
private ScriptTriggerModel _model = new();
// Last type/config seen on Parameters — distinguishes an external change
// (re-parse) from this component's own echo (skip).
private bool _seen;
private string? _lastType;
private string? _lastConfig;
// Text mirrors — @bind needs settable backing fields; kept in sync with the
// model so blank inputs round-trip blank rather than as 0.
private string _attributeName = string.Empty;
private string _operator = ">";
private string? _thresholdText;
private string? _intervalText;
private string _intervalUnit = "ms";
// ── Parse / serialize lifecycle ────────────────────────────────────────
protected override void OnParametersSet()
{
if (_seen && _lastType == TriggerType && _lastConfig == TriggerConfig) return;
_seen = true;
_lastType = TriggerType;
_lastConfig = TriggerConfig;
_rawType = TriggerType;
_kind = ScriptTriggerConfigCodec.ParseKind(TriggerType);
_kindValue = _kind.ToString();
_model = ScriptTriggerConfigCodec.Parse(TriggerConfig, _kind);
SyncMirrors();
}
private void SyncMirrors()
{
_attributeName = _model.AttributeName ?? string.Empty;
_operator = _model.Operator;
_thresholdText = _model.Threshold?.ToString("R", CultureInfo.InvariantCulture);
(_intervalText, _intervalUnit) = SplitInterval(_model.IntervalMs);
}
/// Chooses the largest whole unit (min/sec/ms) that represents the period exactly.
private static (string?, string) SplitInterval(long? ms)
{
if (ms is not { } v) return (null, "ms");
if (v >= 60000 && v % 60000 == 0) return ((v / 60000).ToString(CultureInfo.InvariantCulture), "min");
if (v >= 1000 && v % 1000 == 0) return ((v / 1000).ToString(CultureInfo.InvariantCulture), "sec");
return (v.ToString(CultureInfo.InvariantCulture), "ms");
}
private static long UnitFactor(string unit) => unit switch
{
"min" => 60000,
"sec" => 1000,
_ => 1
};
/// Serializes the current model and raises once.
private async Task Emit()
{
var type = ScriptTriggerConfigCodec.KindToString(_kind);
var config = ScriptTriggerConfigCodec.Serialize(_model, _kind);
_lastType = type;
_lastConfig = config;
await Changed.InvokeAsync(new ScriptTriggerValue(type, config));
}
// ── Trigger type ───────────────────────────────────────────────────────
private async Task OnKindChanged()
{
if (!Enum.TryParse(_kindValue, out var newKind)
|| newKind == ScriptTriggerKind.Unknown)
{
_kindValue = _kind.ToString();
return;
}
// Carry the attribute name across a ValueChange <-> Conditional switch.
var preservedAttr = _model.AttributeName;
_kind = newKind;
_model = new ScriptTriggerModel();
if (newKind is ScriptTriggerKind.ValueChange or ScriptTriggerKind.Conditional)
_model.AttributeName = preservedAttr;
SyncMirrors();
await Emit();
}
// ── Interval ───────────────────────────────────────────────────────────
private RenderFragment RenderInterval() => __builder =>
{
};
private async Task OnIntervalChanged()
{
_model.IntervalMs =
long.TryParse(_intervalText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n) && n > 0
? n * UnitFactor(_intervalUnit)
: null;
await Emit();
}
// ── Conditional ────────────────────────────────────────────────────────
private RenderFragment RenderConditional() => __builder =>
{
Re-runs on a timer while the condition holds, at the script's
Min time between runs interval — set that field below
the script editor, or the trigger fires only once.
}
};
private async Task OnModeChanged() => await Emit();
// ── Attribute picker (ValueChange + Conditional) ───────────────────────
private RenderFragment RenderAttributePicker(string label) => __builder =>
{
};
private async Task OnAttributeChanged()
{
_model.AttributeName = _attributeName;
await Emit();
}
private static int SourceOrder(string source) => source switch
{
"Direct" => 0,
"Inherited" => 1,
"Composed" => 2,
_ => 3
};
// ── Hint ───────────────────────────────────────────────────────────────
private string BuildHint()
{
var attr = string.IsNullOrWhiteSpace(_model.AttributeName)
? "the selected attribute"
: $"\"{_model.AttributeName}\"";
return _kind switch
{
ScriptTriggerKind.Interval =>
_model.IntervalMs is { } ms
? $"Runs every {_intervalText} {UnitLabel(_intervalUnit)} ({ms} ms)."
: "Runs on a fixed timer — set the period above.",
ScriptTriggerKind.ValueChange =>
$"Runs whenever {attr} changes value.",
ScriptTriggerKind.Conditional =>
_model.Threshold is { } t
? (_model.Mode == ScriptTriggerMode.WhileTrue
? $"Re-runs while {attr} {_model.Operator} {t.ToString("0.###", CultureInfo.InvariantCulture)}."
: $"Runs when {attr} changes, if {attr} {_model.Operator} {t.ToString("0.###", CultureInfo.InvariantCulture)}.")
: $"Runs when {attr} changes and meets the configured condition — set a threshold above.",
ScriptTriggerKind.Expression =>
_model.Mode == ScriptTriggerMode.WhileTrue
? "Re-runs while this expression stays true."
: "Runs once each time this expression becomes true.",
_ => string.Empty
};
}
private static string UnitLabel(string unit) => unit switch
{
"min" => "minute(s)",
"sec" => "second(s)",
_ => "millisecond(s)"
};
private static string OperatorLabel(string op) => op switch
{
">" => "greater than",
">=" => "at least",
"<" => "less than",
"<=" => "at most",
"==" => "equals",
"!=" => "not equal",
_ => ""
};
}