feat(native-alarms): per-binding IsMesReportable on native alarm source bindings (ScadaBridge#59)
TemplateNativeAlarmSource.IsMesReportable + tri-state InstanceNativeAlarmSourceOverride .IsMesReportableOverride flatten onto ResolvedNativeAlarmSource; NativeAlarmActor stamps every mirrored condition and InstanceActor the quiet-binding placeholder, so ScriptAlarm, Alarms.MesReportableAsync() and the #60 wire fields carry it. Additive to the 900-999 severity band. Migration AddNativeAlarmSourceMesReportable. Management commands, CLI --mes-reportable, Central UI (template dialog checkbox, instance override select), Transport DTOs/importer/diff and the revision hash (true-only, so existing hashes are unchanged) carry it. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
+23
-5
@@ -653,6 +653,7 @@
|
||||
<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: 150px;">MES reportable</th>
|
||||
<th style="width: 140px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -697,6 +698,15 @@
|
||||
value="@(_nasFilterEdit.GetValueOrDefault(src.Name) ?? "")"
|
||||
@onchange="e => _nasFilterEdit[src.Name] = string.IsNullOrWhiteSpace((string?)e.Value) ? null : ((string?)e.Value)!.Trim()" />
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" data-test="@($"nas-mes-{src.Name}")"
|
||||
value="@MesText(_nasMesEdit.GetValueOrDefault(src.Name))"
|
||||
@onchange="e => _nasMesEdit[src.Name] = ParseMes((string?)e.Value)">
|
||||
<option value="" selected="@(_nasMesEdit.GetValueOrDefault(src.Name) == null)">Inherited (@(src.IsMesReportable ? "reportable" : "not reportable"))</option>
|
||||
<option value="true" selected="@(_nasMesEdit.GetValueOrDefault(src.Name) == true)">Reportable</option>
|
||||
<option value="false" selected="@(_nasMesEdit.GetValueOrDefault(src.Name) == false)">Not reportable</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="@($"Override actions for {src.Name}")">
|
||||
<button class="btn btn-success"
|
||||
@@ -843,6 +853,9 @@
|
||||
private Dictionary<string, string?> _nasConnEdit = new();
|
||||
private Dictionary<string, string?> _nasRefEdit = new();
|
||||
private Dictionary<string, string?> _nasFilterEdit = new();
|
||||
private Dictionary<string, bool?> _nasMesEdit = new();
|
||||
private static string MesText(bool? v) => v switch { true => "true", false => "false", _ => "" };
|
||||
private static bool? ParseMes(string? v) => v switch { "true" => true, "false" => false, _ => null };
|
||||
|
||||
// Native-alarm-source CSV bulk-import result summary — same shape as the
|
||||
// attribute importer's (_csvImport*) trio. Null until an import runs.
|
||||
@@ -971,6 +984,7 @@
|
||||
_nasConnEdit[s.Name] = ovr?.ConnectionNameOverride;
|
||||
_nasRefEdit[s.Name] = ovr?.SourceReferenceOverride;
|
||||
_nasFilterEdit[s.Name] = ovr?.ConditionFilterOverride;
|
||||
_nasMesEdit[s.Name] = ovr?.IsMesReportableOverride;
|
||||
}
|
||||
|
||||
_flattenedAttributes = await BuildFlattenedAttributesAsync();
|
||||
@@ -1751,16 +1765,17 @@
|
||||
var conn = Blank(_nasConnEdit.GetValueOrDefault(sourceName));
|
||||
var sref = Blank(_nasRefEdit.GetValueOrDefault(sourceName));
|
||||
var filt = Blank(_nasFilterEdit.GetValueOrDefault(sourceName));
|
||||
var mes = _nasMesEdit.GetValueOrDefault(sourceName);
|
||||
|
||||
// All blank → no override; clear any existing row.
|
||||
if (conn == null && sref == null && filt == null)
|
||||
if (conn == null && sref == null && filt == null && mes == null)
|
||||
{
|
||||
await ClearNativeOverrideCore(sourceName);
|
||||
_toast.ShowSuccess($"No override on '{sourceName}' (inherited).");
|
||||
return;
|
||||
}
|
||||
|
||||
await UpsertNativeOverrideCore(sourceName, conn, sref, filt);
|
||||
await UpsertNativeOverrideCore(sourceName, conn, sref, filt, mes);
|
||||
_toast.ShowSuccess($"Saved native alarm source override on '{sourceName}'.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1791,15 +1806,16 @@
|
||||
/// so a bulk import commits the whole batch in one go. Shared by the inline Save
|
||||
/// and the CSV import so both take the identical persistence path.
|
||||
/// </summary>
|
||||
private async Task UpsertNativeOverrideCore(string sourceName, string? conn, string? sref, string? filt)
|
||||
private async Task UpsertNativeOverrideCore(string sourceName, string? conn, string? sref, string? filt, bool? mes = null)
|
||||
{
|
||||
var result = await MutationServices.ExecuteConfigurationAsync(new SetInstanceNativeAlarmSourceOverrideCommand(Id, sourceName, conn, sref, filt));
|
||||
var result = await MutationServices.ExecuteConfigurationAsync(new SetInstanceNativeAlarmSourceOverrideCommand(Id, sourceName, conn, sref, filt, mes));
|
||||
if (result is InstanceNativeAlarmSourceOverride saved) _existingNativeOverrides[sourceName] = saved;
|
||||
|
||||
// Keep the inline editor showing what was just persisted.
|
||||
_nasConnEdit[sourceName] = conn;
|
||||
_nasRefEdit[sourceName] = sref;
|
||||
_nasFilterEdit[sourceName] = filt;
|
||||
_nasMesEdit[sourceName] = mes;
|
||||
}
|
||||
|
||||
private async Task ClearNativeOverrideCore(string sourceName)
|
||||
@@ -1809,6 +1825,7 @@
|
||||
_nasConnEdit[sourceName] = null;
|
||||
_nasRefEdit[sourceName] = null;
|
||||
_nasFilterEdit[sourceName] = null;
|
||||
_nasMesEdit[sourceName] = null;
|
||||
}
|
||||
|
||||
private static string? Blank(string? v) => string.IsNullOrWhiteSpace(v) ? null : v.Trim();
|
||||
@@ -1868,7 +1885,8 @@
|
||||
}
|
||||
|
||||
await MutationServices.ExecuteConfigurationAsync(new SetInstanceNativeAlarmSourceOverridesCommand(Id,
|
||||
outcome.Rows.Select(row => new NativeAlarmSourceOverrideEntry(row.SourceName, row.Connection, row.SourceReference, row.Filter)).ToList()));
|
||||
outcome.Rows.Select(row => new NativeAlarmSourceOverrideEntry(row.SourceName, row.Connection, row.SourceReference, row.Filter,
|
||||
_existingNativeOverrides.GetValueOrDefault(row.SourceName)?.IsMesReportableOverride)).ToList()));
|
||||
await OnInitializedAsync();
|
||||
|
||||
_nasCsvImportSucceeded = true;
|
||||
|
||||
@@ -1041,7 +1041,7 @@
|
||||
=> ShowNativeSourceDialogAsync(
|
||||
editId: null,
|
||||
new TemplateNativeAlarmSourceDialog.NativeSourceDraft(
|
||||
string.Empty, string.Empty, string.Empty, null, null, false));
|
||||
string.Empty, string.Empty, string.Empty, null, null, false, false));
|
||||
|
||||
private Task OpenEditNativeSourceDialog(TemplateNativeAlarmSource src)
|
||||
=> ShowNativeSourceDialogAsync(
|
||||
@@ -1052,7 +1052,8 @@
|
||||
src.SourceReference,
|
||||
src.ConditionFilter,
|
||||
src.Description,
|
||||
src.IsLocked));
|
||||
src.IsLocked,
|
||||
src.IsMesReportable));
|
||||
|
||||
private async Task ShowNativeSourceDialogAsync(
|
||||
int? editId,
|
||||
@@ -1070,6 +1071,7 @@
|
||||
InitialFilter="@initial.ConditionFilter"
|
||||
InitialDescription="@initial.Description"
|
||||
InitialIsLocked="initial.IsLocked"
|
||||
InitialIsMesReportable="initial.IsMesReportable"
|
||||
AlarmCapableConnections="connections"
|
||||
OnSaveAsync="d => SaveNativeSourceDraftAsync(editId, d)" />);
|
||||
|
||||
@@ -1097,7 +1099,8 @@
|
||||
await MutationServices.ExecuteConfigurationAsync(new UpdateTemplateNativeAlarmSourceCommand(id, existing.Name,
|
||||
draft.ConnectionName.Trim(), draft.SourceReference.Trim(),
|
||||
string.IsNullOrWhiteSpace(draft.ConditionFilter) ? null : draft.ConditionFilter.Trim(),
|
||||
string.IsNullOrWhiteSpace(draft.Description) ? null : draft.Description.Trim(), draft.IsLocked));
|
||||
string.IsNullOrWhiteSpace(draft.Description) ? null : draft.Description.Trim(), draft.IsLocked,
|
||||
draft.IsMesReportable));
|
||||
_toast.ShowSuccess($"Native alarm source '{existing.Name}' updated.");
|
||||
return null;
|
||||
}
|
||||
@@ -1114,10 +1117,12 @@
|
||||
SourceReference = draft.SourceReference.Trim(),
|
||||
ConditionFilter = string.IsNullOrWhiteSpace(draft.ConditionFilter) ? null : draft.ConditionFilter.Trim(),
|
||||
Description = string.IsNullOrWhiteSpace(draft.Description) ? null : draft.Description.Trim(),
|
||||
IsLocked = draft.IsLocked
|
||||
IsLocked = draft.IsLocked,
|
||||
IsMesReportable = draft.IsMesReportable
|
||||
};
|
||||
await MutationServices.ExecuteConfigurationAsync(new AddTemplateNativeAlarmSourceCommand(source.TemplateId, source.Name,
|
||||
source.ConnectionName, source.SourceReference, source.ConditionFilter, source.Description, source.IsLocked));
|
||||
source.ConnectionName, source.SourceReference, source.ConditionFilter, source.Description, source.IsLocked,
|
||||
source.IsMesReportable));
|
||||
_toast.ShowSuccess($"Native alarm source '{source.Name}' added.");
|
||||
return null;
|
||||
}
|
||||
|
||||
+14
-2
@@ -51,6 +51,12 @@
|
||||
<label class="form-check-label" for="nasLocked">Locked</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" @bind="_nasIsMesReportable" id="nasMesReportable" />
|
||||
<label class="form-check-label" for="nasMesReportable">MES reportable (every condition under this source)</label>
|
||||
</div>
|
||||
</div>
|
||||
@if (_error != null)
|
||||
{
|
||||
<div class="col-12"><div class="text-danger small" data-test="nas-form-error">@_error</div></div>
|
||||
@@ -77,6 +83,7 @@
|
||||
[Parameter] public string? InitialFilter { get; set; }
|
||||
[Parameter] public string? InitialDescription { get; set; }
|
||||
[Parameter] public bool InitialIsLocked { get; set; }
|
||||
[Parameter] public bool InitialIsMesReportable { get; set; }
|
||||
|
||||
/// <summary>Connections whose protocol can surface native alarms (OPC UA / MxGateway).</summary>
|
||||
[Parameter] public IReadOnlyList<DataConnection> AlarmCapableConnections { get; set; }
|
||||
@@ -94,6 +101,7 @@
|
||||
private string? _nasFilter;
|
||||
private string? _nasDescription;
|
||||
private bool _nasIsLocked;
|
||||
private bool _nasIsMesReportable;
|
||||
private string? _error;
|
||||
private bool _busy;
|
||||
|
||||
@@ -105,6 +113,7 @@
|
||||
_nasFilter = InitialFilter;
|
||||
_nasDescription = InitialDescription;
|
||||
_nasIsLocked = InitialIsLocked;
|
||||
_nasIsMesReportable = InitialIsMesReportable;
|
||||
}
|
||||
|
||||
private async Task Submit()
|
||||
@@ -121,7 +130,8 @@
|
||||
_nasSourceRef,
|
||||
_nasFilter,
|
||||
_nasDescription,
|
||||
_nasIsLocked);
|
||||
_nasIsLocked,
|
||||
_nasIsMesReportable);
|
||||
|
||||
var error = await OnSaveAsync(draft);
|
||||
if (error == null)
|
||||
@@ -146,11 +156,13 @@
|
||||
/// <param name="ConditionFilter">Optional condition filter; blank mirrors every condition.</param>
|
||||
/// <param name="Description">Optional free-text description.</param>
|
||||
/// <param name="IsLocked">Whether instances may override the binding.</param>
|
||||
/// <param name="IsMesReportable">Whether every condition under the source is MES-reportable.</param>
|
||||
public sealed record NativeSourceDraft(
|
||||
string Name,
|
||||
string ConnectionName,
|
||||
string SourceReference,
|
||||
string? ConditionFilter,
|
||||
string? Description,
|
||||
bool IsLocked);
|
||||
bool IsLocked,
|
||||
bool IsMesReportable = false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user