feat(deploy): surface connection-level changes in the deployment diff (#10)
ComputeConnectionsDiff existed with tests but was never called and ConfigurationDiff had no slot for it, so standalone connection endpoint/protocol/failover drift never appeared in the deployment diff (only per-attribute binding drift did). Add a ConnectionChanges slot, wire ComputeConnectionsDiff into ComputeDiff, and render the connection section in the deployment diff UI.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
@@ -12,7 +13,10 @@ using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
using ZB.MOM.WW.ScadaBridge.DeploymentManager;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
||||
@@ -292,6 +296,90 @@ public class TopologyPageTests : BunitContext
|
||||
Assert.Throws<Bunit.MissingEventHandlerException>(() => instanceLabel.DoubleClick());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_ConnectionEndpointChange_RendersConnectionSection()
|
||||
{
|
||||
// TemplateEngine-018 / DeploymentManager-018: a standalone connection
|
||||
// endpoint edit (no per-attribute binding change) must surface in the
|
||||
// deployment-diff modal. Before ConnectionChanges was wired through
|
||||
// ComputeDiff + the UI, this redeploy showed only the stale-hash badge
|
||||
// with no indication that the connection endpoint had moved.
|
||||
// The DiffDialog body-scroll lock + focus call out to JS interop on
|
||||
// open; loose mode no-ops the handlers we don't explicitly set up.
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var areasBySite = new Dictionary<int, IReadOnlyList<Area>>
|
||||
{
|
||||
[1] = new List<Area> { new("Line-1") { Id = 10, SiteId = 1 } }
|
||||
};
|
||||
SeedRepos(
|
||||
sites: new[] { new Site("Plant-A", "plant-a") { Id = 1 } },
|
||||
instances: new[]
|
||||
{
|
||||
new Instance("Pump-001") { Id = 100, SiteId = 1, AreaId = 10, State = InstanceState.Enabled }
|
||||
},
|
||||
areasBySite: areasBySite);
|
||||
|
||||
// Deployed snapshot: connection "plc1" points at host-a.
|
||||
var deployedConfig = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Pump-001",
|
||||
Connections = new Dictionary<string, ConnectionConfig>
|
||||
{
|
||||
["plc1"] = new ConnectionConfig
|
||||
{
|
||||
Protocol = "OpcUa",
|
||||
ConfigurationJson = "{\"endpoint\":\"opc.tcp://host-a:4840\"}",
|
||||
FailoverRetryCount = 3,
|
||||
}
|
||||
}
|
||||
};
|
||||
_deployRepo.GetDeployedSnapshotByInstanceIdAsync(100, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<DeployedConfigSnapshot?>(
|
||||
new DeployedConfigSnapshot("dep-1", "hash-old",
|
||||
JsonSerializer.Serialize(deployedConfig))));
|
||||
|
||||
// Current template-derived config: same connection now points at host-b.
|
||||
var currentConfig = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Pump-001",
|
||||
Connections = new Dictionary<string, ConnectionConfig>
|
||||
{
|
||||
["plc1"] = new ConnectionConfig
|
||||
{
|
||||
Protocol = "OpcUa",
|
||||
ConfigurationJson = "{\"endpoint\":\"opc.tcp://host-b:4840\"}",
|
||||
FailoverRetryCount = 3,
|
||||
}
|
||||
}
|
||||
};
|
||||
_pipeline.FlattenAndValidateAsync(100, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(Result<FlatteningPipelineResult>.Success(
|
||||
new FlatteningPipelineResult(currentConfig, "hash-new", ValidationResult.Success()))));
|
||||
|
||||
var cut = Render<TopologyPage>();
|
||||
FindToggleForLabel(cut, "Plant-A")!.Click();
|
||||
FindToggleForLabel(cut, "Line-1")!.Click();
|
||||
|
||||
// The per-node action menu only renders after a context-menu (right
|
||||
// click) on the instance row, so open it first, then click "Diff".
|
||||
var instanceRow = cut.FindAll(".tv-row")
|
||||
.First(row => row.QuerySelector(".tv-label")?.TextContent == "Pump-001");
|
||||
instanceRow.ContextMenu();
|
||||
|
||||
var diffButton = cut.FindAll("button.dropdown-item")
|
||||
.First(b => b.TextContent.Trim() == "Diff");
|
||||
diffButton.Click();
|
||||
|
||||
var markup = cut.Markup;
|
||||
Assert.Contains("Connections", markup);
|
||||
Assert.Contains("plc1", markup);
|
||||
Assert.Contains("host-a", markup);
|
||||
Assert.Contains("host-b", markup);
|
||||
// The change is a modification, so the row carries the "Changed" badge.
|
||||
Assert.Contains("Changed", markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyInstancesRoute_IsDeclaredOnTopologyPage()
|
||||
{
|
||||
|
||||
@@ -369,4 +369,105 @@ public class DiffServiceTests
|
||||
|
||||
Assert.Empty(diff);
|
||||
}
|
||||
|
||||
// ── TemplateEngine-018: ComputeDiff wires ComputeConnectionsDiff into the
|
||||
// public ConfigurationDiff.ConnectionChanges slot so standalone connection
|
||||
// protocol/endpoint/failover drift surfaces in the deployment diff (#10). ──
|
||||
|
||||
[Fact]
|
||||
public void ComputeDiff_ConnectionProtocolAndEndpointAndFailoverChange_PopulatesConnectionChanges()
|
||||
{
|
||||
// Protocol, endpoint config JSON, and failover retry count all differ
|
||||
// on the same connection. Before this wiring, ComputeDiff dropped the
|
||||
// entire connection dimension so this redeploy showed "no changes".
|
||||
var oldConfig = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Connections = new Dictionary<string, ConnectionConfig>
|
||||
{
|
||||
["plc1"] = new ConnectionConfig
|
||||
{
|
||||
Protocol = "OpcUa",
|
||||
ConfigurationJson = "{\"endpoint\":\"opc.tcp://host-a:4840\"}",
|
||||
FailoverRetryCount = 3,
|
||||
}
|
||||
}
|
||||
};
|
||||
var newConfig = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Connections = new Dictionary<string, ConnectionConfig>
|
||||
{
|
||||
["plc1"] = new ConnectionConfig
|
||||
{
|
||||
Protocol = "Modbus",
|
||||
ConfigurationJson = "{\"endpoint\":\"opc.tcp://host-b:4840\"}",
|
||||
FailoverRetryCount = 5,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var diff = _sut.ComputeDiff(oldConfig, newConfig);
|
||||
|
||||
Assert.True(diff.HasChanges);
|
||||
Assert.Single(diff.ConnectionChanges);
|
||||
var entry = diff.ConnectionChanges[0];
|
||||
Assert.Equal("plc1", entry.CanonicalName);
|
||||
Assert.Equal(DiffChangeType.Changed, entry.ChangeType);
|
||||
Assert.Equal("OpcUa", entry.OldValue!.Protocol);
|
||||
Assert.Equal("Modbus", entry.NewValue!.Protocol);
|
||||
Assert.Contains("host-a", entry.OldValue!.ConfigurationJson);
|
||||
Assert.Contains("host-b", entry.NewValue!.ConfigurationJson);
|
||||
Assert.Equal(3, entry.OldValue!.FailoverRetryCount);
|
||||
Assert.Equal(5, entry.NewValue!.FailoverRetryCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDiff_OnlyConnectionDiffers_HasChangesIsTrue()
|
||||
{
|
||||
// Attributes, alarms, and scripts are identical; only a connection's
|
||||
// endpoint changed. HasChanges must be true so the diff view does not
|
||||
// claim "no differences" while a connection endpoint silently moved.
|
||||
var attributes = new List<ResolvedAttribute>
|
||||
{
|
||||
new ResolvedAttribute { CanonicalName = "Temp", Value = "25", DataType = "Double" }
|
||||
};
|
||||
var oldConfig = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Attributes = attributes,
|
||||
Connections = new Dictionary<string, ConnectionConfig>
|
||||
{
|
||||
["plc1"] = new ConnectionConfig
|
||||
{
|
||||
Protocol = "OpcUa",
|
||||
ConfigurationJson = "{\"endpoint\":\"opc.tcp://host-a:4840\"}",
|
||||
FailoverRetryCount = 3,
|
||||
}
|
||||
}
|
||||
};
|
||||
var newConfig = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Attributes = attributes,
|
||||
Connections = new Dictionary<string, ConnectionConfig>
|
||||
{
|
||||
["plc1"] = new ConnectionConfig
|
||||
{
|
||||
Protocol = "OpcUa",
|
||||
ConfigurationJson = "{\"endpoint\":\"opc.tcp://host-b:4840\"}",
|
||||
FailoverRetryCount = 3,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var diff = _sut.ComputeDiff(oldConfig, newConfig);
|
||||
|
||||
Assert.True(diff.HasChanges);
|
||||
Assert.Empty(diff.AttributeChanges);
|
||||
Assert.Empty(diff.AlarmChanges);
|
||||
Assert.Empty(diff.ScriptChanges);
|
||||
Assert.Single(diff.ConnectionChanges);
|
||||
Assert.Equal(DiffChangeType.Changed, diff.ConnectionChanges[0].ChangeType);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user