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>
</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)
{
<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 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>
[Parameter] public IReadOnlyList<AlarmAttributeChoice> AvailableAttributes { get; set; }
= Array.Empty<AlarmAttributeChoice>();
@@ -82,6 +92,7 @@
private AlarmTriggerType _triggerType;
private string? _triggerConfig;
private bool _isLocked;
private bool _isMesReportable;
private string? _error;
private bool _busy;
@@ -92,6 +103,7 @@
_triggerType = InitialTriggerType;
_triggerConfig = InitialTriggerConfig;
_isLocked = InitialIsLocked;
_isMesReportable = InitialIsMesReportable;
}
private async Task Submit()
@@ -103,7 +115,7 @@
_busy = true;
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)
{
Context.Close(true);
@@ -125,10 +137,12 @@
/// <param name="Priority">Priority level.</param>
/// <param name="TriggerConfig">Structured trigger configuration payload.</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(
string Name,
AlarmTriggerType TriggerType,
int Priority,
string? TriggerConfig,
bool IsLocked);
bool IsLocked,
bool IsMesReportable);
}
@@ -907,6 +907,7 @@
<th>Trigger</th>
<th>Priority</th>
<th>Config</th>
<th>MES</th>
<th>Lock</th>
<th style="width: 60px;">Actions</th>
</tr>
@@ -922,6 +923,16 @@
config readable instead of merely hidden. *@
<td class="small text-muted text-truncate" style="max-width: 200px;"
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>
@if (alarm.IsLocked)
{
@@ -1436,7 +1447,7 @@
private Task OpenAddAlarmDialog()
=> ShowAlarmDialogAsync(
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)
=> ShowAlarmDialogAsync(
@@ -1446,7 +1457,8 @@
alarm.TriggerType,
alarm.PriorityLevel,
alarm.TriggerConfiguration,
alarm.IsLocked));
alarm.IsLocked,
alarm.IsMesReportable));
private async Task ShowAlarmDialogAsync(int? editId, TemplateAlarmDialog.AlarmDraft initial)
{
@@ -1461,6 +1473,7 @@
InitialPriority="initial.Priority"
InitialTriggerConfig="@initial.TriggerConfig"
InitialIsLocked="initial.IsLocked"
InitialIsMesReportable="initial.IsMesReportable"
AvailableAttributes="choices"
OnSaveAsync="d => SaveAlarmDraftAsync(editId, d)" />,
size: "modal-dialog-scrollable");
@@ -1521,6 +1534,7 @@
PriorityLevel = draft.Priority,
TriggerConfiguration = draft.TriggerConfig?.Trim(),
IsLocked = draft.IsLocked,
IsMesReportable = draft.IsMesReportable,
Description = existing.Description,
OnTriggerScriptId = existing.OnTriggerScriptId,
};
@@ -1536,7 +1550,8 @@
TriggerType = draft.TriggerType,
PriorityLevel = draft.Priority,
TriggerConfiguration = draft.TriggerConfig?.Trim(),
IsLocked = draft.IsLocked
IsLocked = draft.IsLocked,
IsMesReportable = draft.IsMesReportable
};
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);
}
}