diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/AlarmsHistorian.razor b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/AlarmsHistorian.razor new file mode 100644 index 0000000..b8dcbfe --- /dev/null +++ b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/AlarmsHistorian.razor @@ -0,0 +1,79 @@ +@page "/alarms/historian" +@using ZB.MOM.WW.OtOpcUa.Admin.Services +@using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian +@inject HistorianDiagnosticsService Diag + +

Alarm historian

+

Local store-and-forward queue that ships alarm events to Aveva Historian via Galaxy.Host.

+ +
+
+
+
+ Drain state +

@_status.DrainState

+
+
+ Queue depth +

@_status.QueueDepth.ToString("N0")

+
+
+ Dead-letter depth +

@_status.DeadLetterDepth.ToString("N0")

+
+
+ Last success +

@(_status.LastSuccessUtc?.ToString("u") ?? "—")

+
+
+ + @if (!string.IsNullOrEmpty(_status.LastError)) + { +
+ Last error: @_status.LastError +
+ } +
+
+ +
+ + +
+ +@if (_retryResult is not null) +{ +
Requeued @_retryResult row(s) for retry.
+} + +@code { + private HistorianSinkStatus _status = new(0, 0, null, null, null, HistorianDrainState.Disabled); + private int? _retryResult; + + protected override void OnInitialized() => _status = Diag.GetStatus(); + + private Task RefreshAsync() + { + _status = Diag.GetStatus(); + _retryResult = null; + return Task.CompletedTask; + } + + private Task RetryDeadLetteredAsync() + { + _retryResult = Diag.TryRetryDeadLettered(); + _status = Diag.GetStatus(); + return Task.CompletedTask; + } + + private static string BadgeFor(HistorianDrainState s) => s switch + { + HistorianDrainState.Idle => "bg-success", + HistorianDrainState.Draining => "bg-info", + HistorianDrainState.BackingOff => "bg-warning text-dark", + HistorianDrainState.Disabled => "bg-secondary", + _ => "bg-secondary", + }; +} diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DraftEditor.razor b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DraftEditor.razor index dfef4b9..e92f284 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DraftEditor.razor +++ b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DraftEditor.razor @@ -23,6 +23,7 @@ +
@@ -32,6 +33,7 @@ else if (_tab == "namespaces") { } else if (_tab == "drivers") { } else if (_tab == "acls") { } + else if (_tab == "scripts") { }
diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptEditor.razor b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptEditor.razor new file mode 100644 index 0000000..2f4339c --- /dev/null +++ b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptEditor.razor @@ -0,0 +1,41 @@ +@using Microsoft.AspNetCore.Components.Web +@inject IJSRuntime JS + +@* + Monaco-backed C# code editor (Phase 7 Stream F). Progressive enhancement: + textarea renders immediately, Monaco mounts via JS interop after first render. + Monaco script tags are loaded once from the parent layout (wwwroot/js/monaco-loader.js + pulls the CDN bundle). + + Stream F keeps the interop surface small — bind `Source` two-way, and the parent + tab re-renders on change for the dependency preview. The test-harness button + lives in the parent so one editor can drive multiple script types. +*@ + +
+ +
+ +@code { + [Parameter] public string Source { get; set; } = string.Empty; + [Parameter] public EventCallback SourceChanged { get; set; } + + private readonly string _editorId = $"script-editor-{Guid.NewGuid():N}"; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + try + { + await JS.InvokeVoidAsync("otOpcUaScriptEditor.attach", _editorId); + } + catch (JSException) + { + // Monaco bundle not yet loaded on this page — textarea fallback is + // still functional. + } + } + } +} diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptsTab.razor b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptsTab.razor new file mode 100644 index 0000000..b0d5eb7 --- /dev/null +++ b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptsTab.razor @@ -0,0 +1,224 @@ +@using ZB.MOM.WW.OtOpcUa.Admin.Services +@using ZB.MOM.WW.OtOpcUa.Configuration.Entities +@using ZB.MOM.WW.OtOpcUa.Core.Abstractions +@using ZB.MOM.WW.OtOpcUa.Core.Scripting +@inject ScriptService ScriptSvc +@inject ScriptTestHarnessService Harness + +
+
+

Scripts

+ C# (Roslyn). Used by virtual tags + scripted alarms. +
+ +
+ + + +@if (_loading) {

Loading…

} +else if (_scripts.Count == 0 && _editing is null) +{ +
No scripts yet in this draft.
+} +else +{ +
+
+
+ @foreach (var s in _scripts) + { + + } +
+
+
+ @if (_editing is not null) + { +
+
+ @(_isNew ? "New script" : _editing.Name) +
+ @if (!_isNew) + { + + } + +
+
+
+
+ + +
+ + + +
+ + +
+ + @if (_dependencies is not null) + { +
+ Inferred reads + @if (_dependencies.Reads.Count == 0) { none } + else + { +
    + @foreach (var r in _dependencies.Reads) {
  • @r
  • } +
+ } + Inferred writes + @if (_dependencies.Writes.Count == 0) { none } + else + { +
    + @foreach (var w in _dependencies.Writes) {
  • @w
  • } +
+ } + @if (_dependencies.Rejections.Count > 0) + { +
+ Non-literal paths rejected: +
    + @foreach (var r in _dependencies.Rejections) {
  • @r.Message
  • } +
+
+ } +
+ } + + @if (_testResult is not null) + { +
+ Harness result: @_testResult.Outcome + @if (_testResult.Outcome == ScriptTestOutcome.Success) + { +
Output: @(_testResult.Output?.ToString() ?? "null")
+ @if (_testResult.Writes.Count > 0) + { +
Writes: +
    + @foreach (var kv in _testResult.Writes) {
  • @kv.Key = @(kv.Value?.ToString() ?? "null")
  • } +
+
+ } + } + @if (_testResult.Errors.Count > 0) + { +
+ @foreach (var e in _testResult.Errors) {
@e
} +
+ } + @if (_testResult.LogEvents.Count > 0) + { +
Script log output: +
    + @foreach (var e in _testResult.LogEvents) {
  • [@e.Level] @e.RenderMessage()
  • } +
+
+ } +
+ } +
+
+ } +
+
+} + +@code { + [Parameter] public long GenerationId { get; set; } + [Parameter] public string ClusterId { get; set; } = string.Empty; + + private bool _loading = true; + private bool _busy; + private bool _harnessBusy; + private bool _isNew; + private List