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>
|
||||
|
||||
@* 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 *@
|
||||
<div class="card mb-3">
|
||||
<div class="card-header py-2">
|
||||
@@ -208,6 +278,15 @@
|
||||
private List<TemplateAttribute> _overrideAttrs = 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
|
||||
private List<Area> _siteAreas = new();
|
||||
private int _reassignAreaId;
|
||||
@@ -249,6 +328,19 @@
|
||||
var existingOverrides = await TemplateEngineRepository.GetOverridesByInstanceIdAsync(Id);
|
||||
foreach (var o in existingOverrides)
|
||||
_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)
|
||||
{
|
||||
@@ -333,6 +425,124 @@
|
||||
_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 ────────────────────────────────────────────────
|
||||
|
||||
private async Task ReassignArea()
|
||||
|
||||
Reference in New Issue
Block a user