feat(ui): rich AlarmTriggerEditor in instance override modal
Replaces the per-row JSON textbox with an Edit button that opens a modal hosting the full AlarmTriggerEditor. The editor pre-populates with the merged inherited + override config so the operator sees the effective state, not the override delta. On Save: - HiLo: diff against inherited, store only changed keys - Binary trigger types: whole-replace if the edited config differs Value comparison in the diff is type-aware (decoded strings, numeric GetDouble) so JSON-escape differences (e.g., literal em-dash vs —) don't produce false-positive diffs that pollute the override JSON. FlatteningService.MergeHiLoConfig is now public so the UI can pre-merge the editor seed; new public DiffHiLoConfig handles the symmetric direction. +2 encoding tests cover the new equivalence behavior. The override row's summary column shows the diff'd keys + priority chip so operators see what's overridden at a glance.
This commit is contained in:
@@ -94,6 +94,100 @@ public class FlatteningServiceMergeTests
|
||||
Assert.Equal(3, doc.RootElement.GetProperty("hiDeadband").GetDouble());
|
||||
}
|
||||
|
||||
// ── DiffHiLoConfig ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_NoChanges_ReturnsNull()
|
||||
{
|
||||
const string both = @"{""attributeName"":""Temp"",""hi"":80}";
|
||||
Assert.Null(FlatteningService.DiffHiLoConfig(both, both));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_ChangedKey_ReturnsOnlyChangedKey()
|
||||
{
|
||||
const string inherited = @"{""attributeName"":""Temp"",""loLo"":0,""lo"":10,""hi"":80,""hiHi"":100}";
|
||||
const string edited = @"{""attributeName"":""Temp"",""loLo"":0,""lo"":10,""hi"":90,""hiHi"":100}";
|
||||
|
||||
var diff = FlatteningService.DiffHiLoConfig(inherited, edited);
|
||||
|
||||
Assert.NotNull(diff);
|
||||
using var doc = JsonDocument.Parse(diff!);
|
||||
var prop = Assert.Single(doc.RootElement.EnumerateObject());
|
||||
Assert.Equal("hi", prop.Name);
|
||||
Assert.Equal(90, prop.Value.GetDouble());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_NewKey_AddedToDiff()
|
||||
{
|
||||
const string inherited = @"{""attributeName"":""Temp"",""hi"":80}";
|
||||
const string edited = @"{""attributeName"":""Temp"",""hi"":80,""hiDeadband"":3}";
|
||||
|
||||
var diff = FlatteningService.DiffHiLoConfig(inherited, edited);
|
||||
|
||||
Assert.NotNull(diff);
|
||||
using var doc = JsonDocument.Parse(diff!);
|
||||
Assert.Equal(3, doc.RootElement.GetProperty("hiDeadband").GetDouble());
|
||||
Assert.False(doc.RootElement.TryGetProperty("hi", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_NullInherited_ReturnsEditedVerbatim()
|
||||
{
|
||||
const string edited = @"{""attributeName"":""Temp"",""hi"":80}";
|
||||
Assert.Equal(edited, FlatteningService.DiffHiLoConfig(null, edited));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_NullEdited_ReturnsNull()
|
||||
{
|
||||
Assert.Null(FlatteningService.DiffHiLoConfig(@"{""hi"":80}", null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_IgnoresStringEscapeDifferences()
|
||||
{
|
||||
// Inherited has literal em-dash; edited has the unicode-escaped form.
|
||||
// Decoded values are identical, so the key should NOT be in the diff.
|
||||
const string inherited = @"{""attributeName"":""Temp"",""hi"":80,""hiMessage"":""High — investigate""}";
|
||||
const string edited = @"{""attributeName"":""Temp"",""hi"":80,""hiMessage"":""High — investigate""}";
|
||||
|
||||
var diff = FlatteningService.DiffHiLoConfig(inherited, edited);
|
||||
|
||||
Assert.Null(diff); // no real change once values are decoded
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_IgnoresNumericFormatDifferences()
|
||||
{
|
||||
// 85 vs 85.0 are the same number — should not produce a diff.
|
||||
const string inherited = @"{""hi"":85}";
|
||||
const string edited = @"{""hi"":85.0}";
|
||||
Assert.Null(FlatteningService.DiffHiLoConfig(inherited, edited));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_RoundTripsThroughMerge()
|
||||
{
|
||||
// Merge(inherited, Diff(inherited, edited)) ≡ edited — when the
|
||||
// edited config is itself a superset/equivalent of inherited.
|
||||
const string inherited = @"{""attributeName"":""Temp"",""hi"":80,""hiHi"":100}";
|
||||
const string edited = @"{""attributeName"":""Temp"",""hi"":90,""hiHi"":100,""hiDeadband"":5}";
|
||||
|
||||
var diff = FlatteningService.DiffHiLoConfig(inherited, edited);
|
||||
var merged = FlatteningService.MergeHiLoConfig(inherited, diff);
|
||||
|
||||
using var origDoc = JsonDocument.Parse(edited);
|
||||
using var mergedDoc = JsonDocument.Parse(merged!);
|
||||
Assert.Equal(origDoc.RootElement.GetProperty("hi").GetDouble(),
|
||||
mergedDoc.RootElement.GetProperty("hi").GetDouble());
|
||||
Assert.Equal(origDoc.RootElement.GetProperty("hiHi").GetDouble(),
|
||||
mergedDoc.RootElement.GetProperty("hiHi").GetDouble());
|
||||
Assert.Equal(origDoc.RootElement.GetProperty("hiDeadband").GetDouble(),
|
||||
mergedDoc.RootElement.GetProperty("hiDeadband").GetDouble());
|
||||
}
|
||||
|
||||
// ── Instance-level alarm override (end-to-end Flatten) ─────────────────
|
||||
|
||||
private static (Template, Instance) BuildHiLoFixture(string inheritedJson, InstanceAlarmOverride? ovr = null, bool locked = false)
|
||||
|
||||
Reference in New Issue
Block a user