feat(ui): MES reportable checkbox on the template alarm dialog + MES column on the Alarms tab

Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB
This commit is contained in:
Joseph Doherty
2026-09-11 10:03:49 -04:00
parent 43973d34de
commit c8a1bcef98
3 changed files with 101 additions and 5 deletions
@@ -41,6 +41,13 @@
<label class="form-check-label" for="alarmLocked">Locked</label> <label class="form-check-label" for="alarmLocked">Locked</label>
</div> </div>
</div> </div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="_isMesReportable" id="alarmMesReportable" />
<label class="form-check-label" for="alarmMesReportable">MES reportable</label>
<div class="form-text">Returned by the MES alarm-status API as <code>IsFlaggedForMES</code>. Instances can override this unless the alarm is locked.</div>
</div>
</div>
@if (_error != null) @if (_error != null)
{ {
<div class="col-12"><div class="text-danger small" data-test="alarm-form-error">@_error</div></div> <div class="col-12"><div class="text-danger small" data-test="alarm-form-error">@_error</div></div>
@@ -67,6 +74,9 @@
[Parameter] public string? InitialTriggerConfig { get; set; } [Parameter] public string? InitialTriggerConfig { get; set; }
[Parameter] public bool InitialIsLocked { get; set; } [Parameter] public bool InitialIsLocked { get; set; }
/// <summary>Seeds the "MES reportable" flag returned by the MES alarm-status API.</summary>
[Parameter] public bool InitialIsMesReportable { get; set; }
/// <summary>Attribute choices offered by the structured trigger editor's picker.</summary> /// <summary>Attribute choices offered by the structured trigger editor's picker.</summary>
[Parameter] public IReadOnlyList<AlarmAttributeChoice> AvailableAttributes { get; set; } [Parameter] public IReadOnlyList<AlarmAttributeChoice> AvailableAttributes { get; set; }
= Array.Empty<AlarmAttributeChoice>(); = Array.Empty<AlarmAttributeChoice>();
@@ -82,6 +92,7 @@
private AlarmTriggerType _triggerType; private AlarmTriggerType _triggerType;
private string? _triggerConfig; private string? _triggerConfig;
private bool _isLocked; private bool _isLocked;
private bool _isMesReportable;
private string? _error; private string? _error;
private bool _busy; private bool _busy;
@@ -92,6 +103,7 @@
_triggerType = InitialTriggerType; _triggerType = InitialTriggerType;
_triggerConfig = InitialTriggerConfig; _triggerConfig = InitialTriggerConfig;
_isLocked = InitialIsLocked; _isLocked = InitialIsLocked;
_isMesReportable = InitialIsMesReportable;
} }
private async Task Submit() private async Task Submit()
@@ -103,7 +115,7 @@
_busy = true; _busy = true;
try try
{ {
var error = await OnSaveAsync(new AlarmDraft(_name, _triggerType, _priority, _triggerConfig, _isLocked)); var error = await OnSaveAsync(new AlarmDraft(_name, _triggerType, _priority, _triggerConfig, _isLocked, _isMesReportable));
if (error == null) if (error == null)
{ {
Context.Close(true); Context.Close(true);
@@ -125,10 +137,12 @@
/// <param name="Priority">Priority level.</param> /// <param name="Priority">Priority level.</param>
/// <param name="TriggerConfig">Structured trigger configuration payload.</param> /// <param name="TriggerConfig">Structured trigger configuration payload.</param>
/// <param name="IsLocked">Whether instances may override the alarm.</param> /// <param name="IsLocked">Whether instances may override the alarm.</param>
/// <param name="IsMesReportable">Whether the MES alarm-status API reports this alarm.</param>
public sealed record AlarmDraft( public sealed record AlarmDraft(
string Name, string Name,
AlarmTriggerType TriggerType, AlarmTriggerType TriggerType,
int Priority, int Priority,
string? TriggerConfig, string? TriggerConfig,
bool IsLocked); bool IsLocked,
bool IsMesReportable);
} }
@@ -907,6 +907,7 @@
<th>Trigger</th> <th>Trigger</th>
<th>Priority</th> <th>Priority</th>
<th>Config</th> <th>Config</th>
<th>MES</th>
<th>Lock</th> <th>Lock</th>
<th style="width: 60px;">Actions</th> <th style="width: 60px;">Actions</th>
</tr> </tr>
@@ -922,6 +923,16 @@
config readable instead of merely hidden. *@ config readable instead of merely hidden. *@
<td class="small text-muted text-truncate" style="max-width: 200px;" <td class="small text-muted text-truncate" style="max-width: 200px;"
title="@alarm.TriggerConfiguration">@(alarm.TriggerConfiguration ?? "—")</td> title="@alarm.TriggerConfiguration">@(alarm.TriggerConfiguration ?? "—")</td>
<td data-test="alarm-mes-badge">
@if (alarm.IsMesReportable)
{
<span class="badge bg-success" aria-label="MES reportable">MES</span>
}
else
{
<span class="badge bg-light text-dark" aria-label="Not MES reportable">—</span>
}
</td>
<td> <td>
@if (alarm.IsLocked) @if (alarm.IsLocked)
{ {
@@ -1436,7 +1447,7 @@
private Task OpenAddAlarmDialog() private Task OpenAddAlarmDialog()
=> ShowAlarmDialogAsync( => ShowAlarmDialogAsync(
editId: null, editId: null,
new TemplateAlarmDialog.AlarmDraft(string.Empty, default, 500, null, false)); new TemplateAlarmDialog.AlarmDraft(string.Empty, default, 500, null, false, false));
private Task OpenEditAlarmDialog(TemplateAlarm alarm) private Task OpenEditAlarmDialog(TemplateAlarm alarm)
=> ShowAlarmDialogAsync( => ShowAlarmDialogAsync(
@@ -1446,7 +1457,8 @@
alarm.TriggerType, alarm.TriggerType,
alarm.PriorityLevel, alarm.PriorityLevel,
alarm.TriggerConfiguration, alarm.TriggerConfiguration,
alarm.IsLocked)); alarm.IsLocked,
alarm.IsMesReportable));
private async Task ShowAlarmDialogAsync(int? editId, TemplateAlarmDialog.AlarmDraft initial) private async Task ShowAlarmDialogAsync(int? editId, TemplateAlarmDialog.AlarmDraft initial)
{ {
@@ -1461,6 +1473,7 @@
InitialPriority="initial.Priority" InitialPriority="initial.Priority"
InitialTriggerConfig="@initial.TriggerConfig" InitialTriggerConfig="@initial.TriggerConfig"
InitialIsLocked="initial.IsLocked" InitialIsLocked="initial.IsLocked"
InitialIsMesReportable="initial.IsMesReportable"
AvailableAttributes="choices" AvailableAttributes="choices"
OnSaveAsync="d => SaveAlarmDraftAsync(editId, d)" />, OnSaveAsync="d => SaveAlarmDraftAsync(editId, d)" />,
size: "modal-dialog-scrollable"); size: "modal-dialog-scrollable");
@@ -1521,6 +1534,7 @@
PriorityLevel = draft.Priority, PriorityLevel = draft.Priority,
TriggerConfiguration = draft.TriggerConfig?.Trim(), TriggerConfiguration = draft.TriggerConfig?.Trim(),
IsLocked = draft.IsLocked, IsLocked = draft.IsLocked,
IsMesReportable = draft.IsMesReportable,
Description = existing.Description, Description = existing.Description,
OnTriggerScriptId = existing.OnTriggerScriptId, OnTriggerScriptId = existing.OnTriggerScriptId,
}; };
@@ -1536,7 +1550,8 @@
TriggerType = draft.TriggerType, TriggerType = draft.TriggerType,
PriorityLevel = draft.Priority, PriorityLevel = draft.Priority,
TriggerConfiguration = draft.TriggerConfig?.Trim(), TriggerConfiguration = draft.TriggerConfig?.Trim(),
IsLocked = draft.IsLocked IsLocked = draft.IsLocked,
IsMesReportable = draft.IsMesReportable
}; };
var addResult = await TemplateService.AddAlarmAsync(_selectedTemplate.Id, alarm, user); var addResult = await TemplateService.AddAlarmAsync(_selectedTemplate.Id, alarm, user);
@@ -0,0 +1,67 @@
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Design;
/// <summary>
/// The template alarm authoring surface exposes <c>TemplateAlarm.IsMesReportable</c>:
/// an "MES reportable" checkbox in the dialog body and an MES column on the
/// template's Alarms tab. <c>TemplateEdit</c> is a heavyweight page (~10 injected
/// services with their own graphs), so — like the rest of the TemplateEdit coverage
/// in this suite (see <c>TemplateNativeAlarmSourceEditorTests</c>) — these are
/// structural assertions over the component source that pin the wiring so it cannot
/// silently regress. The persistence behaviour itself is covered by the
/// TemplateService / ManagementActor alarm tests.
/// </summary>
public class TemplateAlarmDialogMesReportableTests
{
private static string DesignComponentsRoot
{
get
{
var dir = AppContext.BaseDirectory;
for (var i = 0; i < 6 && dir is not null; i++)
dir = Directory.GetParent(dir)?.FullName;
return Path.Combine(dir!, "src", "ZB.MOM.WW.ScadaBridge.CentralUI",
"Components", "Pages", "Design");
}
}
private static string AlarmDialogMarkup
=> File.ReadAllText(Path.Combine(DesignComponentsRoot, "TemplateAlarmDialog.razor"));
private static string TemplateEditMarkup
=> File.ReadAllText(Path.Combine(DesignComponentsRoot, "TemplateEdit.razor"));
[Fact]
public void AlarmDialog_HasMesReportableCheckbox()
{
var markup = AlarmDialogMarkup;
Assert.Contains("@bind=\"_isMesReportable\"", markup);
Assert.Contains("id=\"alarmMesReportable\"", markup);
Assert.Contains("MES reportable", markup);
}
[Fact]
public void AlarmDialog_ThreadsMesReportableThroughParameterAndDraft()
{
var markup = AlarmDialogMarkup;
Assert.Contains("InitialIsMesReportable", markup);
Assert.Contains("_isMesReportable = InitialIsMesReportable", markup);
Assert.Contains("bool IsMesReportable", markup);
}
[Fact]
public void AlarmsTab_HasMesColumn()
{
var markup = TemplateEditMarkup;
Assert.Contains("<th>MES</th>", markup);
Assert.Contains("data-test=\"alarm-mes-badge\"", markup);
Assert.Contains("alarm.IsMesReportable", markup);
}
[Fact]
public void AlarmsTab_PassesMesReportableIntoAndOutOfTheDialog()
{
var markup = TemplateEditMarkup;
Assert.Contains("InitialIsMesReportable=\"initial.IsMesReportable\"", markup);
Assert.Contains("IsMesReportable = draft.IsMesReportable", markup);
}
}