feat(ui): instance alarm override editor in InstanceConfigure
Adds an Alarm Overrides card to the per-instance Configure page (next to the existing Attribute Overrides and Connection Bindings cards). Each non-locked template alarm gets a row showing its trigger type, inherited config, and inputs for an override JSON + priority override. A Clear button removes the override; the Save Alarm Overrides button upserts all dirty rows. The HiLo merge / binary whole-replace semantics are surfaced via the JSON placeholder hint per trigger type. Wired to the existing InstanceService.SetAlarmOverrideAsync / DeleteAlarmOverrideAsync flow.
This commit is contained in:
@@ -165,6 +165,76 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@* Alarm Overrides *@
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header py-2">
|
||||||
|
<strong>Alarm Overrides</strong>
|
||||||
|
<small class="text-muted ms-2">
|
||||||
|
For HiLo alarms, override JSON merges into the inherited config setpoint-by-setpoint.
|
||||||
|
For binary trigger types, override JSON replaces the whole config. Leave both fields empty to inherit.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
@if (_overridableAlarms.Count == 0)
|
||||||
|
{
|
||||||
|
<p class="text-muted small p-3 mb-0">No overridable (non-locked) alarms on this template.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table table-sm table-bordered mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Alarm</th>
|
||||||
|
<th style="width: 110px;">Trigger</th>
|
||||||
|
<th>Inherited Config</th>
|
||||||
|
<th style="width: 320px;">Override JSON</th>
|
||||||
|
<th style="width: 120px;">Priority</th>
|
||||||
|
<th style="width: 80px;"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var alarm in _overridableAlarms)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="small">@alarm.Name</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-light text-dark border">@alarm.TriggerType</span>
|
||||||
|
</td>
|
||||||
|
<td class="small text-muted text-truncate font-monospace" style="max-width: 280px;"
|
||||||
|
title="@alarm.TriggerConfiguration">
|
||||||
|
@(alarm.TriggerConfiguration ?? "—")
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" class="form-control form-control-sm font-monospace"
|
||||||
|
placeholder='@GetAlarmJsonPlaceholder(alarm.TriggerType)'
|
||||||
|
value="@GetAlarmOverrideJson(alarm.Name)"
|
||||||
|
@onchange="(e) => OnAlarmOverrideJsonChanged(alarm.Name, e)" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" min="0" max="1000" class="form-control form-control-sm"
|
||||||
|
placeholder="@alarm.PriorityLevel"
|
||||||
|
value="@GetAlarmOverridePriority(alarm.Name)"
|
||||||
|
@onchange="(e) => OnAlarmOverridePriorityChanged(alarm.Name, e)" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (HasOverride(alarm.Name))
|
||||||
|
{
|
||||||
|
<button class="btn btn-outline-danger btn-sm"
|
||||||
|
@onclick="() => ClearAlarmOverride(alarm.Name)"
|
||||||
|
disabled="@_saving">Clear</button>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="p-2">
|
||||||
|
<button class="btn btn-success btn-sm" @onclick="SaveAlarmOverrides" disabled="@_saving">Save Alarm Overrides</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@* Area Assignment *@
|
@* Area Assignment *@
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header py-2">
|
<div class="card-header py-2">
|
||||||
@@ -208,6 +278,15 @@
|
|||||||
private List<TemplateAttribute> _overrideAttrs = new();
|
private List<TemplateAttribute> _overrideAttrs = new();
|
||||||
private Dictionary<string, string?> _overrideValues = new();
|
private Dictionary<string, string?> _overrideValues = new();
|
||||||
|
|
||||||
|
// Alarm overrides — keyed by alarm name. Dirty rows live in
|
||||||
|
// _alarmOverrideJson / _alarmOverridePriority; existing overrides are
|
||||||
|
// loaded via _existingAlarmOverrides so the UI can show inherited
|
||||||
|
// placeholders correctly.
|
||||||
|
private List<TemplateAlarm> _overridableAlarms = new();
|
||||||
|
private Dictionary<string, InstanceAlarmOverride> _existingAlarmOverrides = new();
|
||||||
|
private Dictionary<string, string?> _alarmOverrideJson = new();
|
||||||
|
private Dictionary<string, int?> _alarmOverridePriority = new();
|
||||||
|
|
||||||
// Area
|
// Area
|
||||||
private List<Area> _siteAreas = new();
|
private List<Area> _siteAreas = new();
|
||||||
private int _reassignAreaId;
|
private int _reassignAreaId;
|
||||||
@@ -249,6 +328,19 @@
|
|||||||
var existingOverrides = await TemplateEngineRepository.GetOverridesByInstanceIdAsync(Id);
|
var existingOverrides = await TemplateEngineRepository.GetOverridesByInstanceIdAsync(Id);
|
||||||
foreach (var o in existingOverrides)
|
foreach (var o in existingOverrides)
|
||||||
_overrideValues[o.AttributeName] = o.OverrideValue;
|
_overrideValues[o.AttributeName] = o.OverrideValue;
|
||||||
|
|
||||||
|
// Alarm overrides — load all non-locked template alarms and
|
||||||
|
// existing override rows. Pre-seed the dirty maps from existing
|
||||||
|
// values so the inputs render with what's currently saved.
|
||||||
|
var alarms = await TemplateEngineRepository.GetAlarmsByTemplateIdAsync(_instance.TemplateId);
|
||||||
|
_overridableAlarms = alarms.Where(a => !a.IsLocked).ToList();
|
||||||
|
var alarmOverrides = await TemplateEngineRepository.GetAlarmOverridesByInstanceIdAsync(Id);
|
||||||
|
foreach (var o in alarmOverrides)
|
||||||
|
{
|
||||||
|
_existingAlarmOverrides[o.AlarmCanonicalName] = o;
|
||||||
|
_alarmOverrideJson[o.AlarmCanonicalName] = o.TriggerConfigurationOverride;
|
||||||
|
_alarmOverridePriority[o.AlarmCanonicalName] = o.PriorityLevelOverride;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -333,6 +425,124 @@
|
|||||||
_saving = false;
|
_saving = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Alarm overrides ─────────────────────────────────────
|
||||||
|
|
||||||
|
private bool HasOverride(string alarmName) =>
|
||||||
|
_existingAlarmOverrides.ContainsKey(alarmName);
|
||||||
|
|
||||||
|
private string? GetAlarmOverrideJson(string alarmName) =>
|
||||||
|
_alarmOverrideJson.GetValueOrDefault(alarmName);
|
||||||
|
|
||||||
|
private string? GetAlarmOverridePriority(string alarmName) =>
|
||||||
|
_alarmOverridePriority.GetValueOrDefault(alarmName)?.ToString();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Placeholder hint shown in the JSON override input. Encourages the
|
||||||
|
/// HiLo "partial JSON" idiom (e.g. {"hi":90}) vs. the binary-trigger
|
||||||
|
/// "whole-config" idiom.
|
||||||
|
/// </summary>
|
||||||
|
private static string GetAlarmJsonPlaceholder(AlarmTriggerType type) => type switch
|
||||||
|
{
|
||||||
|
AlarmTriggerType.HiLo => "{\"hi\":90} — partial keys merge",
|
||||||
|
_ => "{\"attributeName\":\"...\"} — whole-replace"
|
||||||
|
};
|
||||||
|
|
||||||
|
private void OnAlarmOverrideJsonChanged(string alarmName, ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
var val = e.Value?.ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(val))
|
||||||
|
_alarmOverrideJson.Remove(alarmName);
|
||||||
|
else
|
||||||
|
_alarmOverrideJson[alarmName] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAlarmOverridePriorityChanged(string alarmName, ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
var raw = e.Value?.ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
{
|
||||||
|
_alarmOverridePriority.Remove(alarmName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (int.TryParse(raw, out var p))
|
||||||
|
_alarmOverridePriority[alarmName] = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ClearAlarmOverride(string alarmName)
|
||||||
|
{
|
||||||
|
_saving = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = await GetCurrentUserAsync();
|
||||||
|
var result = await InstanceService.DeleteAlarmOverrideAsync(Id, alarmName, user);
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
_existingAlarmOverrides.Remove(alarmName);
|
||||||
|
_alarmOverrideJson.Remove(alarmName);
|
||||||
|
_alarmOverridePriority.Remove(alarmName);
|
||||||
|
_toast.ShowSuccess($"Cleared override on '{alarmName}'.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_toast.ShowError($"Clear failed: {result.Error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_toast.ShowError($"Clear failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
_saving = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveAlarmOverrides()
|
||||||
|
{
|
||||||
|
_saving = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = await GetCurrentUserAsync();
|
||||||
|
var touched = _alarmOverrideJson.Keys
|
||||||
|
.Union(_alarmOverridePriority.Keys, StringComparer.Ordinal)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var saved = 0;
|
||||||
|
foreach (var alarmName in touched)
|
||||||
|
{
|
||||||
|
var json = _alarmOverrideJson.GetValueOrDefault(alarmName);
|
||||||
|
var priority = _alarmOverridePriority.GetValueOrDefault(alarmName);
|
||||||
|
|
||||||
|
// If both fields are empty AND there's an existing override,
|
||||||
|
// clear it. Otherwise upsert (allows priority-only or json-only).
|
||||||
|
if (string.IsNullOrWhiteSpace(json) && !priority.HasValue)
|
||||||
|
{
|
||||||
|
if (_existingAlarmOverrides.ContainsKey(alarmName))
|
||||||
|
{
|
||||||
|
await InstanceService.DeleteAlarmOverrideAsync(Id, alarmName, user);
|
||||||
|
_existingAlarmOverrides.Remove(alarmName);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await InstanceService.SetAlarmOverrideAsync(
|
||||||
|
Id, alarmName, json, priority, user);
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
{
|
||||||
|
_toast.ShowError($"Save failed for '{alarmName}': {result.Error}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_existingAlarmOverrides[alarmName] = result.Value!;
|
||||||
|
saved++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saved > 0) _toast.ShowSuccess($"Saved {saved} alarm override(s).");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_toast.ShowError($"Save alarm overrides failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
_saving = false;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Area ────────────────────────────────────────────────
|
// ── Area ────────────────────────────────────────────────
|
||||||
|
|
||||||
private async Task ReassignArea()
|
private async Task ReassignArea()
|
||||||
|
|||||||
Reference in New Issue
Block a user