feat(ui): instance configure native alarm source override panel

This commit is contained in:
Joseph Doherty
2026-05-31 02:46:54 -04:00
parent b03ab11d8a
commit 046797e699
2 changed files with 256 additions and 0 deletions
@@ -347,6 +347,88 @@
</div>
}
@* Native Alarm Source Overrides *@
<div class="card mb-3">
<div class="card-header py-2">
<strong>Native Alarm Source Overrides</strong>
<small class="text-muted ms-2">
Retarget an inherited native alarm source binding for this instance.
Leave a field blank to keep the inherited value.
</small>
</div>
<div class="card-body p-0">
@if (_nativeSources.Count == 0)
{
<p class="text-muted small p-3 mb-0">No native alarm sources on this template.</p>
}
else
{
<table class="table table-sm table-bordered mb-0 align-middle">
<thead class="table-light">
<tr>
<th>Source</th>
<th>Inherited</th>
<th style="width: 220px;">Connection override</th>
<th style="width: 220px;">Source reference override</th>
<th style="width: 170px;">Filter override</th>
<th style="width: 140px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var src in _nativeSources)
{
<tr>
<td class="small">
@src.Name
@if (HasNativeOverride(src.Name))
{
<span class="badge bg-warning text-dark ms-1" title="Override is set">●</span>
}
</td>
<td class="small text-muted font-monospace text-truncate" style="max-width: 200px;"
title="@($"{src.ConnectionName} / {src.SourceReference}")">
@src.ConnectionName / @src.SourceReference
</td>
<td>
<select class="form-select form-select-sm"
value="@(_nasConnEdit.GetValueOrDefault(src.Name) ?? "")"
@onchange="e => _nasConnEdit[src.Name] = string.IsNullOrEmpty((string?)e.Value) ? null : (string?)e.Value">
<option value="">(inherited)</option>
@foreach (var c in AlarmCapableConnections())
{
<option value="@c.Name" selected="@(_nasConnEdit.GetValueOrDefault(src.Name) == c.Name)">@c.Name (@c.Protocol)</option>
}
</select>
</td>
<td>
<input class="form-control form-control-sm font-monospace"
placeholder="@src.SourceReference"
value="@(_nasRefEdit.GetValueOrDefault(src.Name) ?? "")"
@onchange="e => _nasRefEdit[src.Name] = string.IsNullOrWhiteSpace((string?)e.Value) ? null : ((string?)e.Value)!.Trim()" />
</td>
<td>
<input class="form-control form-control-sm"
placeholder="@(string.IsNullOrEmpty(src.ConditionFilter) ? "(all)" : src.ConditionFilter)"
value="@(_nasFilterEdit.GetValueOrDefault(src.Name) ?? "")"
@onchange="e => _nasFilterEdit[src.Name] = string.IsNullOrWhiteSpace((string?)e.Value) ? null : ((string?)e.Value)!.Trim()" />
</td>
<td>
<button class="btn btn-success btn-sm me-1"
@onclick="() => SaveNativeOverride(src.Name)" disabled="@_saving">Save</button>
@if (HasNativeOverride(src.Name))
{
<button class="btn btn-outline-danger btn-sm"
@onclick="() => ClearNativeOverride(src.Name)" disabled="@_saving">Clear</button>
}
</td>
</tr>
}
</tbody>
</table>
}
</div>
</div>
@* Area Assignment *@
<div class="card mb-3">
<div class="card-header py-2">
@@ -428,6 +510,15 @@
private List<TemplateAlarm> _overridableAlarms = new();
private Dictionary<string, InstanceAlarmOverride> _existingAlarmOverrides = new();
// Native alarm source overrides — the template's source bindings plus any
// per-instance override rows. Editing is inline (connection / source-ref /
// filter; blank = inherited).
private List<TemplateNativeAlarmSource> _nativeSources = new();
private Dictionary<string, InstanceNativeAlarmSourceOverride> _existingNativeOverrides = new();
private Dictionary<string, string?> _nasConnEdit = new();
private Dictionary<string, string?> _nasRefEdit = new();
private Dictionary<string, string?> _nasFilterEdit = new();
// Override edit modal state — non-null while the modal is open.
private TemplateAlarm? _editingAlarm;
private string? _editingOverrideValue; // current Value parameter for AlarmTriggerEditor
@@ -514,6 +605,23 @@
_existingAlarmOverrides[o.AlarmCanonicalName] = o;
}
// Native alarm source bindings + per-instance overrides. Seed the
// inline edit maps from existing override rows (blank = inherited).
_nativeSources = (await TemplateEngineRepository.GetNativeAlarmSourcesByTemplateIdAsync(_instance.TemplateId)).ToList();
_existingNativeOverrides = new();
var nativeOverrides = await TemplateEngineRepository.GetNativeAlarmSourceOverridesByInstanceIdAsync(Id);
foreach (var o in nativeOverrides)
{
_existingNativeOverrides[o.SourceCanonicalName] = o;
}
foreach (var s in _nativeSources)
{
var ovr = _existingNativeOverrides.GetValueOrDefault(s.Name);
_nasConnEdit[s.Name] = ovr?.ConnectionNameOverride;
_nasRefEdit[s.Name] = ovr?.SourceReferenceOverride;
_nasFilterEdit[s.Name] = ovr?.ConditionFilterOverride;
}
_flattenedAttributes = await BuildFlattenedAttributesAsync();
}
catch (Exception ex)
@@ -892,6 +1000,94 @@
_saving = false;
}
// ── Native alarm source overrides (repository-direct; blank field = inherited) ──
private bool HasNativeOverride(string sourceName) => _existingNativeOverrides.ContainsKey(sourceName);
private IEnumerable<DataConnection> AlarmCapableConnections() =>
_siteConnections.Where(c => string.Equals(c.Protocol, "OpcUa", StringComparison.OrdinalIgnoreCase)
|| string.Equals(c.Protocol, "MxGateway", StringComparison.OrdinalIgnoreCase));
private async Task SaveNativeOverride(string sourceName)
{
_saving = true;
try
{
var conn = Blank(_nasConnEdit.GetValueOrDefault(sourceName));
var sref = Blank(_nasRefEdit.GetValueOrDefault(sourceName));
var filt = Blank(_nasFilterEdit.GetValueOrDefault(sourceName));
// All blank → no override; clear any existing row.
if (conn == null && sref == null && filt == null)
{
await ClearNativeOverrideCore(sourceName);
_toast.ShowSuccess($"No override on '{sourceName}' (inherited).");
return;
}
var existing = await TemplateEngineRepository.GetNativeAlarmSourceOverrideAsync(Id, sourceName);
if (existing == null)
{
var ovr = new InstanceNativeAlarmSourceOverride(sourceName)
{
InstanceId = Id,
ConnectionNameOverride = conn,
SourceReferenceOverride = sref,
ConditionFilterOverride = filt
};
await TemplateEngineRepository.AddInstanceNativeAlarmSourceOverrideAsync(ovr);
await TemplateEngineRepository.SaveChangesAsync();
_existingNativeOverrides[sourceName] = ovr;
}
else
{
existing.ConnectionNameOverride = conn;
existing.SourceReferenceOverride = sref;
existing.ConditionFilterOverride = filt;
await TemplateEngineRepository.UpdateInstanceNativeAlarmSourceOverrideAsync(existing);
await TemplateEngineRepository.SaveChangesAsync();
_existingNativeOverrides[sourceName] = existing;
}
_toast.ShowSuccess($"Saved native alarm source override on '{sourceName}'.");
}
catch (Exception ex)
{
_toast.ShowError($"Save failed: {ex.Message}");
}
_saving = false;
}
private async Task ClearNativeOverride(string sourceName)
{
_saving = true;
try
{
await ClearNativeOverrideCore(sourceName);
_toast.ShowSuccess($"Cleared override on '{sourceName}'.");
}
catch (Exception ex)
{
_toast.ShowError($"Clear failed: {ex.Message}");
}
_saving = false;
}
private async Task ClearNativeOverrideCore(string sourceName)
{
var existing = await TemplateEngineRepository.GetNativeAlarmSourceOverrideAsync(Id, sourceName);
if (existing != null)
{
await TemplateEngineRepository.DeleteInstanceNativeAlarmSourceOverrideAsync(existing.Id);
await TemplateEngineRepository.SaveChangesAsync();
}
_existingNativeOverrides.Remove(sourceName);
_nasConnEdit[sourceName] = null;
_nasRefEdit[sourceName] = null;
_nasFilterEdit[sourceName] = null;
}
private static string? Blank(string? v) => string.IsNullOrWhiteSpace(v) ? null : v.Trim();
/// <summary>
/// Mirrors TemplateEdit.MapDataType — converts the persisted DataType enum
/// to the canonical SCADA type string the AlarmTriggerEditor compares