feat(uns): Alarms tab + ScriptedAlarmModal on the equipment page
This commit is contained in:
@@ -34,7 +34,7 @@ else
|
||||
<li class="nav-item"><button type="button" class="nav-link @TabClass("details")" @onclick='() => _activeTab = "details"'>Details</button></li>
|
||||
<li class="nav-item"><button type="button" class="nav-link @TabClass("tags")" @onclick='() => ShowTabAsync("tags")' disabled="@IsNew">Tags</button></li>
|
||||
<li class="nav-item"><button type="button" class="nav-link @TabClass("vtags")" @onclick='() => ShowTabAsync("vtags")' disabled="@IsNew">Virtual Tags</button></li>
|
||||
<li class="nav-item"><button type="button" class="nav-link @TabClass("alarms")" @onclick='() => _activeTab = "alarms"' disabled="@IsNew">Alarms</button></li>
|
||||
<li class="nav-item"><button type="button" class="nav-link @TabClass("alarms")" @onclick='() => ShowTabAsync("alarms")' disabled="@IsNew">Alarms</button></li>
|
||||
</ul>
|
||||
|
||||
@if (_activeTab == "details")
|
||||
@@ -226,7 +226,52 @@ else
|
||||
Existing="_vtagModalExisting" Scripts="_vtagScriptOptions"
|
||||
OnSaved="OnVirtualTagSavedAsync" OnCancel="@(() => { _vtagModalVisible = false; })" />
|
||||
}
|
||||
else if (_activeTab == "alarms") { <p class="text-muted">Alarms tab — wired in a later task.</p> }
|
||||
else if (_activeTab == "alarms")
|
||||
{
|
||||
<div class="d-flex justify-content-end mb-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" @onclick="OpenAddAlarm">Add alarm</button>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(_alarmError))
|
||||
{
|
||||
<div class="text-danger small mb-2">@_alarmError</div>
|
||||
}
|
||||
@if (_alarms is null)
|
||||
{
|
||||
<p class="text-muted"><span class="spinner-border spinner-border-sm me-1"></span>Loading…</p>
|
||||
}
|
||||
else if (_alarms.Count == 0)
|
||||
{
|
||||
<p class="text-muted">No alarms yet.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Type</th><th>Severity</th><th>Predicate</th><th>Enabled</th><th class="text-end">Actions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var a in _alarms)
|
||||
{
|
||||
<tr @key="a.ScriptedAlarmId">
|
||||
<td>@a.Name</td>
|
||||
<td>@a.AlarmType</td>
|
||||
<td>@a.Severity</td>
|
||||
<td class="mono">@a.PredicateScriptId</td>
|
||||
<td>@(a.Enabled ? "Yes" : "No")</td>
|
||||
<td class="text-end">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm me-1" @onclick="() => OpenEditAlarm(a.ScriptedAlarmId)">Edit</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" @onclick="() => DeleteAlarm(a.ScriptedAlarmId)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
<ScriptedAlarmModal Visible="_alarmModalVisible" IsNew="_alarmModalIsNew" EquipmentId="@EquipmentId"
|
||||
Existing="_alarmModalExisting" Scripts="_alarmScriptOptions"
|
||||
OnSaved="OnAlarmSavedAsync" OnCancel="@(() => { _alarmModalVisible = false; })" />
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
@@ -263,6 +308,14 @@ else
|
||||
private VirtualTagEditDto? _vtagModalExisting;
|
||||
private IReadOnlyList<(string Id, string Display)> _vtagScriptOptions = Array.Empty<(string, string)>();
|
||||
|
||||
// --- Alarms tab state. _alarms is null until the tab is first activated. ---
|
||||
private IReadOnlyList<EquipmentAlarmRow>? _alarms;
|
||||
private string? _alarmError;
|
||||
private bool _alarmModalVisible;
|
||||
private bool _alarmModalIsNew;
|
||||
private ScriptedAlarmEditDto? _alarmModalExisting;
|
||||
private IReadOnlyList<(string Id, string Display)> _alarmScriptOptions = Array.Empty<(string, string)>();
|
||||
|
||||
private string TabClass(string tab) => _activeTab == tab ? "active" : "";
|
||||
|
||||
/// <summary>
|
||||
@@ -278,6 +331,7 @@ else
|
||||
if (IsNew) { return; }
|
||||
if (tab == "tags" && _tags is null) { await ReloadTagsAsync(); }
|
||||
else if (tab == "vtags" && _vtags is null) { await ReloadVirtualTagsAsync(); }
|
||||
else if (tab == "alarms" && _alarms is null) { await ReloadAlarmsAsync(); }
|
||||
}
|
||||
|
||||
// --- Tags tab handlers (mirror GlobalUns; the owning equipment is fixed = EquipmentId) ---
|
||||
@@ -370,6 +424,51 @@ else
|
||||
else { _vtagError = r.Error; }
|
||||
}
|
||||
|
||||
// --- Alarms tab handlers (mirror the Tags/Virtual Tags tabs; the owning equipment is fixed = EquipmentId) ---
|
||||
|
||||
private async Task ReloadAlarmsAsync()
|
||||
{
|
||||
_alarms = await Svc.LoadAlarmsForEquipmentAsync(EquipmentId!);
|
||||
}
|
||||
|
||||
private async Task OpenAddAlarm()
|
||||
{
|
||||
_alarmError = null;
|
||||
_alarmModalIsNew = true;
|
||||
_alarmModalExisting = null;
|
||||
_alarmScriptOptions = await Svc.LoadScriptsAsync();
|
||||
_alarmModalVisible = true;
|
||||
}
|
||||
|
||||
private async Task OpenEditAlarm(string scriptedAlarmId)
|
||||
{
|
||||
_alarmError = null;
|
||||
var dto = await Svc.LoadScriptedAlarmAsync(scriptedAlarmId);
|
||||
if (dto is null) { _alarmError = "That alarm no longer exists; the list was refreshed."; await ReloadAlarmsAsync(); return; }
|
||||
_alarmModalIsNew = false;
|
||||
_alarmModalExisting = dto;
|
||||
_alarmScriptOptions = await Svc.LoadScriptsAsync();
|
||||
_alarmModalVisible = true;
|
||||
}
|
||||
|
||||
private async Task OnAlarmSavedAsync()
|
||||
{
|
||||
_alarmModalVisible = false;
|
||||
await ReloadAlarmsAsync();
|
||||
}
|
||||
|
||||
private async Task DeleteAlarm(string scriptedAlarmId)
|
||||
{
|
||||
_alarmModalVisible = false;
|
||||
_alarmError = null;
|
||||
// Load the alarm fresh to capture its current RowVersion for the concurrency-guarded delete.
|
||||
var dto = await Svc.LoadScriptedAlarmAsync(scriptedAlarmId);
|
||||
if (dto is null) { await ReloadAlarmsAsync(); return; }
|
||||
var r = await Svc.DeleteScriptedAlarmAsync(scriptedAlarmId, dto.RowVersion);
|
||||
if (r.Ok) { await ReloadAlarmsAsync(); }
|
||||
else { _alarmError = r.Error; }
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
_loading = true;
|
||||
@@ -381,6 +480,7 @@ else
|
||||
// lazy loaders re-fetch for the (possibly different) equipment this parameter set targets.
|
||||
_tags = null;
|
||||
_vtags = null;
|
||||
_alarms = null;
|
||||
if (!IsNew)
|
||||
{
|
||||
_equipment = await Svc.LoadEquipmentAsync(EquipmentId!);
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
@* Create/edit modal for an equipment-bound scripted alarm, wired straight into IUnsTreeService. The host
|
||||
page owns visibility and supplies the owning equipment id (create) or the loaded ScriptedAlarmEditDto
|
||||
(edit), plus the candidate predicate-script list. The owning equipment is fixed by the tab, so there is
|
||||
no equipment selector. On a successful save it raises OnSaved so the host can refresh the equipment's
|
||||
alarms in place. Lifted from the standalone ScriptedAlarmEdit page (retired in a later task). *@
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||
@inject IUnsTreeService Svc
|
||||
|
||||
@if (Visible)
|
||||
{
|
||||
<div class="modal-backdrop fade show" style="display:block"></div>
|
||||
<div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<EditForm Model="_form" OnValidSubmit="SaveAsync" FormName="scriptedAlarmModal">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@(IsNew ? "New scripted alarm" : "Edit scripted alarm")</h5>
|
||||
<button type="button" class="btn-close" aria-label="Close" @onclick="CancelAsync"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="alarm-id">ScriptedAlarmId</label>
|
||||
<InputText id="alarm-id" @bind-Value="_form.ScriptedAlarmId" disabled="@(!IsNew)"
|
||||
class="form-control form-control-sm mono" />
|
||||
<ValidationMessage For="@(() => _form.ScriptedAlarmId)" />
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="alarm-name">Name</label>
|
||||
<InputText id="alarm-name" @bind-Value="_form.Name" class="form-control form-control-sm" />
|
||||
<ValidationMessage For="@(() => _form.Name)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="alarm-type">AlarmType</label>
|
||||
<InputSelect id="alarm-type" @bind-Value="_form.AlarmType" class="form-select form-select-sm">
|
||||
<option value="LimitAlarm">LimitAlarm</option>
|
||||
<option value="DiscreteAlarm">DiscreteAlarm</option>
|
||||
<option value="OffNormalAlarm">OffNormalAlarm</option>
|
||||
<option value="AlarmCondition">AlarmCondition</option>
|
||||
</InputSelect>
|
||||
<ValidationMessage For="@(() => _form.AlarmType)" />
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="alarm-severity">Severity (1-1000)</label>
|
||||
<InputNumber id="alarm-severity" @bind-Value="_form.Severity" class="form-control form-control-sm" />
|
||||
<ValidationMessage For="@(() => _form.Severity)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="alarm-script">Predicate script</label>
|
||||
<InputSelect id="alarm-script" @bind-Value="_form.PredicateScriptId" class="form-select form-select-sm">
|
||||
<option value="">— pick script —</option>
|
||||
@foreach (var (id, display) in Scripts)
|
||||
{
|
||||
<option value="@id">@display</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<ValidationMessage For="@(() => _form.PredicateScriptId)" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="alarm-message">Message template</label>
|
||||
<InputTextArea id="alarm-message" @bind-Value="_form.MessageTemplate" rows="3"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="{equipment.MachineCode} temperature out of range: {value}°C" />
|
||||
<ValidationMessage For="@(() => _form.MessageTemplate)" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">HistorizeToAveva</label>
|
||||
<div class="form-check form-switch">
|
||||
<InputCheckbox @bind-Value="_form.HistorizeToAveva" class="form-check-input" />
|
||||
<label class="form-check-label">Route to Wonderware sidecar</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Retain</label>
|
||||
<div class="form-check form-switch">
|
||||
<InputCheckbox @bind-Value="_form.Retain" class="form-check-input" />
|
||||
<label class="form-check-label">Retain active alarms on restart</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Enabled</label>
|
||||
<div class="form-check form-switch">
|
||||
<InputCheckbox @bind-Value="_form.Enabled" class="form-check-input" />
|
||||
<label class="form-check-label">Spawn this alarm in deployments</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(_error))
|
||||
{
|
||||
<div class="text-danger small mt-2">@_error</div>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" @onclick="CancelAsync" disabled="@_busy">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" disabled="@_busy">
|
||||
@if (_busy) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||
@(IsNew ? "Create" : "Save changes")
|
||||
</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>Whether the modal is shown. The host owns this flag.</summary>
|
||||
[Parameter] public bool Visible { get; set; }
|
||||
|
||||
/// <summary><c>true</c> to create a new scripted alarm; <c>false</c> to edit <see cref="Existing"/>.</summary>
|
||||
[Parameter] public bool IsNew { get; set; }
|
||||
|
||||
/// <summary>The owning equipment id the created alarm binds to (used only on create).</summary>
|
||||
[Parameter] public string? EquipmentId { get; set; }
|
||||
|
||||
/// <summary>The scripted alarm being edited, when <see cref="IsNew"/> is <c>false</c>.</summary>
|
||||
[Parameter] public ScriptedAlarmEditDto? Existing { get; set; }
|
||||
|
||||
/// <summary>The candidate predicate scripts as <c>(Id, Display)</c> pairs.</summary>
|
||||
[Parameter] public IReadOnlyList<(string Id, string Display)> Scripts { get; set; } = Array.Empty<(string, string)>();
|
||||
|
||||
/// <summary>Raised after a successful create/save so the host can refresh the equipment's alarms and close.</summary>
|
||||
[Parameter] public EventCallback OnSaved { get; set; }
|
||||
|
||||
/// <summary>Raised when the user cancels so the host can close.</summary>
|
||||
[Parameter] public EventCallback OnCancel { get; set; }
|
||||
|
||||
private FormModel _form = new();
|
||||
private bool _busy;
|
||||
private string? _error;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
// Rebuild the working form whenever the host (re)opens the modal for a fresh target.
|
||||
if (IsNew)
|
||||
{
|
||||
_form = new FormModel();
|
||||
}
|
||||
else if (Existing is not null)
|
||||
{
|
||||
_form = new FormModel
|
||||
{
|
||||
ScriptedAlarmId = Existing.ScriptedAlarmId,
|
||||
Name = Existing.Name,
|
||||
AlarmType = Existing.AlarmType,
|
||||
Severity = Existing.Severity,
|
||||
MessageTemplate = Existing.MessageTemplate,
|
||||
PredicateScriptId = Existing.PredicateScriptId,
|
||||
HistorizeToAveva = Existing.HistorizeToAveva,
|
||||
Retain = Existing.Retain,
|
||||
Enabled = Existing.Enabled,
|
||||
};
|
||||
}
|
||||
_error = null;
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
_busy = true;
|
||||
_error = null;
|
||||
try
|
||||
{
|
||||
var input = new ScriptedAlarmInput(_form.ScriptedAlarmId, _form.Name, _form.AlarmType, _form.Severity,
|
||||
_form.MessageTemplate, _form.PredicateScriptId, _form.HistorizeToAveva, _form.Retain, _form.Enabled);
|
||||
|
||||
var result = IsNew
|
||||
? await Svc.CreateScriptedAlarmAsync(EquipmentId!, input)
|
||||
: await Svc.UpdateScriptedAlarmAsync(Existing!.ScriptedAlarmId, input, Existing.RowVersion);
|
||||
|
||||
if (result.Ok)
|
||||
{
|
||||
await OnSaved.InvokeAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_error = result.Error;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Task CancelAsync() => OnCancel.InvokeAsync();
|
||||
|
||||
private sealed class FormModel
|
||||
{
|
||||
[Required, RegularExpression("^[A-Za-z0-9_-]+$")] public string ScriptedAlarmId { get; set; } = "";
|
||||
[Required] public string Name { get; set; } = "";
|
||||
[Required] public string AlarmType { get; set; } = "LimitAlarm";
|
||||
[Range(1, 1000)] public int Severity { get; set; } = 500;
|
||||
[Required] public string PredicateScriptId { get; set; } = "";
|
||||
[Required] public string MessageTemplate { get; set; } = "";
|
||||
public bool HistorizeToAveva { get; set; } = true;
|
||||
public bool Retain { get; set; } = true;
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user