@namespace ScadaLink.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 } 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.
@(string.IsNullOrWhiteSpace(TriggerConfig) ? "(empty)" : TriggerConfig)
break; } @* ── 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 => {
@RenderAttributePicker("Monitored attribute")
}; private async Task OnOperatorChanged() { _model.Operator = ScriptTriggerConfigCodec.NormalizeOperator(_operator); await Emit(); } private async Task OnThresholdChanged() { _model.Threshold = double.TryParse(_thresholdText, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : null; await Emit(); } // ── Expression ───────────────────────────────────────────────────────── private RenderFragment RenderExpression() => __builder => {
A boolean C# expression — e.g. Attributes["Temperature"] > 80.
}; private async Task OnExpressionChanged(string value) { _model.Expression = value; 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 ? $"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 => "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", _ => "" }; }