feat(uns): Alarms tab + ScriptedAlarmModal on the equipment page

This commit is contained in:
Joseph Doherty
2026-06-11 14:53:03 -04:00
parent 1294fb6ee5
commit 773c073d0e
2 changed files with 309 additions and 2 deletions
@@ -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!);