diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor index db92a690..55e33d63 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor @@ -347,6 +347,88 @@ } + @* Native Alarm Source Overrides *@ +
+
+ Native Alarm Source Overrides + + Retarget an inherited native alarm source binding for this instance. + Leave a field blank to keep the inherited value. + +
+
+ @if (_nativeSources.Count == 0) + { +

No native alarm sources on this template.

+ } + else + { + + + + + + + + + + + + + @foreach (var src in _nativeSources) + { + + + + + + + + + } + +
SourceInheritedConnection overrideSource reference overrideFilter overrideActions
+ @src.Name + @if (HasNativeOverride(src.Name)) + { + + } + + @src.ConnectionName / @src.SourceReference + + + + + + + + + @if (HasNativeOverride(src.Name)) + { + + } +
+ } +
+
+ @* Area Assignment *@
@@ -428,6 +510,15 @@ private List _overridableAlarms = new(); private Dictionary _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 _nativeSources = new(); + private Dictionary _existingNativeOverrides = new(); + private Dictionary _nasConnEdit = new(); + private Dictionary _nasRefEdit = new(); + private Dictionary _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 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(); + /// /// Mirrors TemplateEdit.MapDataType — converts the persisted DataType enum /// to the canonical SCADA type string the AlarmTriggerEditor compares diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/InstanceConfigureNativeAlarmTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/InstanceConfigureNativeAlarmTests.cs new file mode 100644 index 00000000..b005590d --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/InstanceConfigureNativeAlarmTests.cs @@ -0,0 +1,60 @@ +namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment; + +/// +/// Task 25: Instance Configure exposes a "Native Alarm Source Overrides" card — +/// per-instance retarget of an inherited native alarm source binding (connection / +/// source reference / filter; blank = inherited). InstanceConfigure is a +/// heavyweight page (7 injected services incl. InstanceService and the +/// flattening pipeline), so — consistent with the template-editor coverage — these +/// are structural assertions over the component source that pin the card and its +/// repository-direct upsert/clear wiring. The override CRUD itself is covered +/// behaviorally by the ManagementActor native-alarm-source handler tests. +/// +public class InstanceConfigureNativeAlarmTests +{ + private static string InstanceConfigureMarkup + { + get + { + var dir = AppContext.BaseDirectory; + for (var i = 0; i < 6 && dir is not null; i++) + dir = Directory.GetParent(dir)?.FullName; + return File.ReadAllText(Path.Combine(dir!, "src", "ZB.MOM.WW.ScadaBridge.CentralUI", + "Components", "Pages", "Deployment", "InstanceConfigure.razor")); + } + } + + [Fact] + public void InstanceConfigure_HasNativeAlarmSourceOverridesCard() + { + var markup = InstanceConfigureMarkup; + Assert.Contains("Native Alarm Source Overrides", markup); + Assert.Contains("Leave a field blank to keep the inherited value", markup); + Assert.Contains("HasNativeOverride", markup); + } + + [Fact] + public void NativeOverrideRow_HasConnectionSourceRefAndFilterEditors() + { + var markup = InstanceConfigureMarkup; + Assert.Contains("AlarmCapableConnections", markup); // dropdown filtered to OPC UA / MxGateway + Assert.Contains("_nasConnEdit", markup); + Assert.Contains("_nasRefEdit", markup); + Assert.Contains("_nasFilterEdit", markup); + Assert.Contains("(inherited)", markup); + } + + [Fact] + public void NativeOverride_WiresRepositoryUpsertAndClear() + { + var markup = InstanceConfigureMarkup; + Assert.Contains("GetNativeAlarmSourceOverridesByInstanceIdAsync", markup); + Assert.Contains("GetNativeAlarmSourceOverrideAsync", markup); + Assert.Contains("AddInstanceNativeAlarmSourceOverrideAsync", markup); + Assert.Contains("UpdateInstanceNativeAlarmSourceOverrideAsync", markup); + Assert.Contains("DeleteInstanceNativeAlarmSourceOverrideAsync", markup); + Assert.Contains("SaveChangesAsync", markup); + Assert.Contains("SaveNativeOverride", markup); + Assert.Contains("ClearNativeOverride", markup); + } +}