41f133a337
Gap 2 (#25): VirtualTagsTab.razor + /virtual-tags global page — list/create/toggle virtual tags per draft generation with DataType, Script, trigger, Historize, Enabled fields. Tab wired into DraftEditor. Gap 3 (#26): ScriptedAlarmsTab.razor + /scripted-alarms global page — list/create scripted alarms with AlarmType, Severity, MessageTemplate, PredicateScript, HistorizeToAveva, Retain. SeverityBand helper shows Low/Medium/High/Critical label. Tab wired into DraftEditor. Gap 4 (#27): ScriptLogHub (SignalR IAsyncEnumerable stream) tails scripts-*.log with optional ScriptName filter; ScriptLog.razor provides Start/Stop/Clear controls plus level filter dropdown. Hub registered at /hubs/script-log in Program.cs. Nav rail gains a "Scripting" eyebrow with entries for all three pages. 19 new unit tests for ScriptLogHub parse/filter/tail helpers (Category=Unit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
192 lines
7.3 KiB
Plaintext
192 lines
7.3 KiB
Plaintext
@page "/scripted-alarms"
|
|
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
|
@rendermode RenderMode.InteractiveServer
|
|
@using Microsoft.AspNetCore.Components.Web
|
|
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
|
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
|
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
|
@inject ScriptedAlarmService AlarmSvc
|
|
@inject GenerationService GenerationSvc
|
|
@inject ClusterService ClusterSvc
|
|
@inject NavigationManager Nav
|
|
|
|
<h1 class="page-title">Scripted Alarms</h1>
|
|
<p class="text-muted">
|
|
OPC UA Part 9 alarms raised by C# predicate scripts. To author scripted alarms, open a cluster
|
|
draft and use the <strong>Scripted Alarms</strong> tab in the draft editor.
|
|
This view lists all scripted alarms across clusters and generations for reference.
|
|
</p>
|
|
|
|
<div class="toolbar mb-3">
|
|
<select class="form-select form-select-sm tb-state" @bind="_filterClusterId" @bind:after="OnClusterChangedAsync">
|
|
<option value="">— all clusters —</option>
|
|
@foreach (var c in _clusters)
|
|
{
|
|
<option value="@c.ClusterId">@c.Name (@c.ClusterId)</option>
|
|
}
|
|
</select>
|
|
<select class="form-select form-select-sm tb-state ms-2" @bind="_filterGenerationId" @bind:after="LoadAlarmsAsync"
|
|
disabled="@(string.IsNullOrEmpty(_filterClusterId))">
|
|
<option value="0">— all generations —</option>
|
|
@foreach (var g in _generations)
|
|
{
|
|
<option value="@g.GenerationId">gen @g.GenerationId (@g.Status) @(g.PublishedAt?.ToString("yyyy-MM-dd") ?? "")</option>
|
|
}
|
|
</select>
|
|
<span class="spacer"></span>
|
|
@if (_alarms is not null) { <span class="tb-count">@_alarms.Count alarm@(_alarms.Count == 1 ? "" : "s")</span> }
|
|
</div>
|
|
|
|
@if (_loading)
|
|
{
|
|
<p class="text-muted">Loading…</p>
|
|
}
|
|
else if (_alarms is null || _alarms.Count == 0)
|
|
{
|
|
<section class="panel notice rise" style="animation-delay:.02s">
|
|
No scripted alarms found for the selected filter.
|
|
Open a cluster draft and use the <strong>Scripted Alarms</strong> tab to author scripted alarms.
|
|
</section>
|
|
}
|
|
else
|
|
{
|
|
<section class="panel rise" style="animation-delay:.08s">
|
|
<div class="panel-head">Scripted alarms</div>
|
|
<div class="table-wrap">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Equipment</th>
|
|
<th>Type</th>
|
|
<th class="num">Severity</th>
|
|
<th>Message template</th>
|
|
<th>Predicate script</th>
|
|
<th>Historize</th>
|
|
<th>Retain</th>
|
|
<th>Enabled</th>
|
|
<th class="num">Generation</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var a in _alarms)
|
|
{
|
|
<tr>
|
|
<td><span class="mono">@a.Name</span></td>
|
|
<td><span class="mono">@a.EquipmentId</span></td>
|
|
<td><span class="chip chip-idle">@a.AlarmType</span></td>
|
|
<td class="num">@a.Severity <small class="text-muted">@SeverityBand(a.Severity)</small></td>
|
|
<td class="text-truncate" style="max-width:18rem" title="@a.MessageTemplate">
|
|
<span class="mono small">@a.MessageTemplate</span>
|
|
</td>
|
|
<td><span class="mono">@a.PredicateScriptId</span></td>
|
|
<td>
|
|
@if (a.HistorizeToAveva) { <span class="chip chip-ok">Aveva</span> }
|
|
else { <span class="text-muted">—</span> }
|
|
</td>
|
|
<td>
|
|
@if (a.Retain) { <span class="chip chip-ok">yes</span> }
|
|
else { <span class="text-muted">—</span> }
|
|
</td>
|
|
<td>
|
|
@if (a.Enabled) { <span class="chip chip-ok">enabled</span> }
|
|
else { <span class="chip chip-idle">disabled</span> }
|
|
</td>
|
|
<td class="num">@a.GenerationId</td>
|
|
<td>
|
|
@if (DraftLink(a.GenerationId) is { } link)
|
|
{
|
|
<a class="btn btn-sm btn-outline-secondary" href="@link">Edit draft</a>
|
|
}
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
@code {
|
|
private List<ServerCluster> _clusters = [];
|
|
private List<ConfigGeneration> _generations = [];
|
|
private List<ScriptedAlarm>? _alarms;
|
|
private string _filterClusterId = string.Empty;
|
|
private long _filterGenerationId;
|
|
private bool _loading;
|
|
private Dictionary<long, (string clusterId, bool isDraft)> _genMap = [];
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
_clusters = await ClusterSvc.ListAsync(CancellationToken.None);
|
|
await LoadAlarmsAsync();
|
|
}
|
|
|
|
private async Task OnClusterChangedAsync()
|
|
{
|
|
_filterGenerationId = 0;
|
|
_generations = string.IsNullOrEmpty(_filterClusterId)
|
|
? []
|
|
: await GenerationSvc.ListRecentAsync(_filterClusterId, 20, CancellationToken.None);
|
|
await LoadAlarmsAsync();
|
|
}
|
|
|
|
private async Task LoadAlarmsAsync()
|
|
{
|
|
_loading = true;
|
|
_alarms = null;
|
|
try
|
|
{
|
|
var all = new List<ScriptedAlarm>();
|
|
_genMap = [];
|
|
|
|
IEnumerable<ConfigGeneration> gens;
|
|
if (!string.IsNullOrEmpty(_filterClusterId) && _filterGenerationId != 0)
|
|
{
|
|
gens = _generations.Where(g => g.GenerationId == _filterGenerationId);
|
|
}
|
|
else if (!string.IsNullOrEmpty(_filterClusterId))
|
|
{
|
|
gens = await GenerationSvc.ListRecentAsync(_filterClusterId, 20, CancellationToken.None);
|
|
_generations = gens.ToList();
|
|
}
|
|
else
|
|
{
|
|
var allGens = new List<ConfigGeneration>();
|
|
foreach (var c in _clusters)
|
|
{
|
|
var cGens = await GenerationSvc.ListRecentAsync(c.ClusterId, 5, CancellationToken.None);
|
|
allGens.AddRange(cGens);
|
|
}
|
|
gens = allGens;
|
|
}
|
|
|
|
foreach (var g in gens)
|
|
{
|
|
_genMap[g.GenerationId] = (g.ClusterId, g.Status == GenerationStatus.Draft);
|
|
var alarms = await AlarmSvc.ListAsync(g.GenerationId, CancellationToken.None);
|
|
all.AddRange(alarms);
|
|
}
|
|
|
|
_alarms = all;
|
|
}
|
|
finally { _loading = false; }
|
|
}
|
|
|
|
private string? DraftLink(long generationId)
|
|
{
|
|
if (_genMap.TryGetValue(generationId, out var info) && info.isDraft)
|
|
return $"/clusters/{info.clusterId}/draft/{generationId}";
|
|
return null;
|
|
}
|
|
|
|
private static string SeverityBand(int s) => s switch
|
|
{
|
|
<= 250 => "(Low)",
|
|
<= 500 => "(Medium)",
|
|
<= 750 => "(High)",
|
|
_ => "(Critical)",
|
|
};
|
|
}
|