refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
@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 { } *@
|
||||
|
||||
<div class="border rounded bg-white p-3">
|
||||
|
||||
@* ── Trigger type ──────────────────────────────────────────────────── *@
|
||||
<div class="mb-3">
|
||||
<label for="script-trigger-type" class="form-label small text-uppercase text-muted fw-semibold mb-1">
|
||||
Trigger type
|
||||
</label>
|
||||
<select id="script-trigger-type" class="form-select form-select-sm"
|
||||
@bind="_kindValue" @bind:after="OnKindChanged">
|
||||
<option value="None">— none (never runs automatically) —</option>
|
||||
<option value="Interval">Interval — run on a fixed timer</option>
|
||||
<option value="ValueChange">Value change — run when an attribute changes</option>
|
||||
<option value="Conditional">Conditional — run when a condition is met</option>
|
||||
<option value="Expression">Expression — run when a boolean expression becomes true</option>
|
||||
<option value="Call">Call — run only when invoked by another script</option>
|
||||
@if (_kind == ScriptTriggerKind.Unknown)
|
||||
{
|
||||
<optgroup label="Unrecognized">
|
||||
<option value="Unknown">@_rawType (unknown)</option>
|
||||
</optgroup>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@* ── Type-specific configuration ───────────────────────────────────── *@
|
||||
@switch (_kind)
|
||||
{
|
||||
case ScriptTriggerKind.Interval:
|
||||
@RenderInterval();
|
||||
break;
|
||||
case ScriptTriggerKind.ValueChange:
|
||||
<div class="mb-1">@RenderAttributePicker("Monitored attribute")</div>
|
||||
break;
|
||||
case ScriptTriggerKind.Conditional:
|
||||
@RenderConditional();
|
||||
break;
|
||||
case ScriptTriggerKind.Expression:
|
||||
@RenderExpression();
|
||||
break;
|
||||
case ScriptTriggerKind.Call:
|
||||
<div class="small text-muted">
|
||||
No automatic trigger — this script runs only when another script
|
||||
invokes it via <code>Instance.CallScript("...")</code>.
|
||||
</div>
|
||||
break;
|
||||
case ScriptTriggerKind.Unknown:
|
||||
<div class="alert alert-warning py-2 small mb-0">
|
||||
Unrecognized trigger type <code>@_rawType</code>. Its stored
|
||||
configuration is shown below and left untouched — pick a known
|
||||
trigger type above to reconfigure it.
|
||||
<pre class="bg-light border rounded p-2 mt-2 mb-0">@(string.IsNullOrWhiteSpace(TriggerConfig) ? "(empty)" : TriggerConfig)</pre>
|
||||
</div>
|
||||
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)
|
||||
{
|
||||
<div class="mt-3 pt-2 border-top small text-muted">@BuildHint()</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
// ── Parameters ─────────────────────────────────────────────────────────
|
||||
|
||||
[Parameter] public string? TriggerType { get; set; }
|
||||
[Parameter] public string? TriggerConfig { get; set; }
|
||||
|
||||
/// <summary>Raised whenever the type or config changes — emits both atomically.</summary>
|
||||
[Parameter] public EventCallback<ScriptTriggerValue> Changed { get; set; }
|
||||
|
||||
/// <summary>Flattened attribute list (direct + inherited + composed) for the picker.</summary>
|
||||
[Parameter] public IReadOnlyList<AlarmAttributeChoice> AvailableAttributes { get; set; } =
|
||||
Array.Empty<AlarmAttributeChoice>();
|
||||
|
||||
// ── 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);
|
||||
}
|
||||
|
||||
/// <summary>Chooses the largest whole unit (min/sec/ms) that represents the period exactly.</summary>
|
||||
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
|
||||
};
|
||||
|
||||
/// <summary>Serializes the current model and raises <see cref="Changed"/> once.</summary>
|
||||
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<ScriptTriggerKind>(_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 =>
|
||||
{
|
||||
<div class="row g-2 align-items-end" style="max-width: 420px;">
|
||||
<div class="col-7">
|
||||
<label class="form-label small text-uppercase text-muted fw-semibold mb-1">Run every</label>
|
||||
<input type="number" min="1" step="1" class="form-control form-control-sm"
|
||||
placeholder="period"
|
||||
@bind="_intervalText" @bind:event="oninput" @bind:after="OnIntervalChanged" />
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<select class="form-select form-select-sm"
|
||||
@bind="_intervalUnit" @bind:after="OnIntervalChanged">
|
||||
<option value="ms">milliseconds</option>
|
||||
<option value="sec">seconds</option>
|
||||
<option value="min">minutes</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
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 =>
|
||||
{
|
||||
<div class="mb-2">@RenderAttributePicker("Monitored attribute")</div>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label small text-uppercase text-muted fw-semibold mb-1">Operator</label>
|
||||
<select class="form-select form-select-sm"
|
||||
@bind="_operator" @bind:after="OnOperatorChanged">
|
||||
@foreach (var op in ScriptTriggerConfigCodec.Operators)
|
||||
{
|
||||
<option value="@op">@op — @OperatorLabel(op)</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<label class="form-label small text-uppercase text-muted fw-semibold mb-1">Threshold</label>
|
||||
<input type="number" step="any" class="form-control form-control-sm"
|
||||
placeholder="numeric value"
|
||||
@bind="_thresholdText" @bind:event="oninput" @bind:after="OnThresholdChanged" />
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
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 =>
|
||||
{
|
||||
<label class="form-label small text-uppercase text-muted fw-semibold mb-1">Trigger expression</label>
|
||||
<MonacoEditor Height="120px"
|
||||
Language="csharp"
|
||||
ScriptKind="ScriptAnalysis.ScriptKind.Template"
|
||||
ShowToolbar="false"
|
||||
Value="@(_model.Expression ?? string.Empty)"
|
||||
ValueChanged="OnExpressionChanged"
|
||||
SelfAttributes="@TriggerAttributeMapper.SelfAttributes(AvailableAttributes)"
|
||||
Children="@TriggerAttributeMapper.Children(AvailableAttributes)" />
|
||||
<div class="form-text">
|
||||
A boolean C# expression — e.g. <code>Attributes["Temperature"] > 80</code>.
|
||||
</div>
|
||||
};
|
||||
|
||||
private async Task OnExpressionChanged(string value)
|
||||
{
|
||||
_model.Expression = value;
|
||||
await Emit();
|
||||
}
|
||||
|
||||
// ── Fire mode (Conditional + Expression) ───────────────────────────────
|
||||
|
||||
private RenderFragment RenderMode() => __builder =>
|
||||
{
|
||||
<div class="mt-3">
|
||||
<label for="script-trigger-mode" class="form-label small text-uppercase text-muted fw-semibold mb-1">
|
||||
Fire mode
|
||||
</label>
|
||||
<select id="script-trigger-mode" class="form-select form-select-sm"
|
||||
style="max-width: 420px;"
|
||||
@bind="_model.Mode" @bind:after="OnModeChanged">
|
||||
<option value="@ScriptTriggerMode.OnTrue">
|
||||
Once — when the condition becomes true
|
||||
</option>
|
||||
<option value="@ScriptTriggerMode.WhileTrue">
|
||||
Repeatedly — while the condition stays true
|
||||
</option>
|
||||
</select>
|
||||
@if (_model.Mode == ScriptTriggerMode.WhileTrue)
|
||||
{
|
||||
<div class="form-text">
|
||||
Re-runs on a timer while the condition holds, at the script's
|
||||
<strong>Min time between runs</strong> interval — set that field below
|
||||
the script editor, or the trigger fires only once.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
};
|
||||
|
||||
private async Task OnModeChanged() => await Emit();
|
||||
|
||||
// ── Attribute picker (ValueChange + Conditional) ───────────────────────
|
||||
|
||||
private RenderFragment RenderAttributePicker(string label) => __builder =>
|
||||
{
|
||||
<label class="form-label small text-uppercase text-muted fw-semibold mb-1">@label</label>
|
||||
<select class="form-select form-select-sm"
|
||||
@bind="_attributeName" @bind:after="OnAttributeChanged">
|
||||
<option value="">— select attribute —</option>
|
||||
@{
|
||||
var groups = AvailableAttributes
|
||||
.GroupBy(c => c.Source)
|
||||
.OrderBy(g => SourceOrder(g.Key))
|
||||
.ToList();
|
||||
}
|
||||
@foreach (var grp in groups)
|
||||
{
|
||||
<optgroup label="@grp.Key">
|
||||
@foreach (var choice in grp.OrderBy(c => c.CanonicalName, StringComparer.Ordinal))
|
||||
{
|
||||
<option value="@choice.CanonicalName">@choice.CanonicalName (@choice.DataType)</option>
|
||||
}
|
||||
</optgroup>
|
||||
}
|
||||
@* Keep a saved-but-missing attribute selectable so it stays visible. *@
|
||||
@if (!string.IsNullOrEmpty(_attributeName)
|
||||
&& !AvailableAttributes.Any(c => string.Equals(c.CanonicalName, _attributeName, StringComparison.Ordinal)))
|
||||
{
|
||||
<optgroup label="Unknown">
|
||||
<option value="@_attributeName">@_attributeName (not on this template)</option>
|
||||
</optgroup>
|
||||
}
|
||||
</select>
|
||||
};
|
||||
|
||||
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",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user