diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DiffSection.razor b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DiffSection.razor
new file mode 100644
index 0000000..bb0207d
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DiffSection.razor
@@ -0,0 +1,90 @@
+@using ZB.MOM.WW.OtOpcUa.Admin.Services
+
+@* Per-section diff renderer — the base used by DiffViewer for every known TableName. Caps
+ output at RowCap rows so a pathological draft (e.g. 20k tags churned) can't freeze the
+ Blazor render; overflow banner tells operator how many rows were hidden. *@
+
+
+
+ @if (_total == 0)
+ {
+
No changes in this section.
+ }
+ else
+ {
+ @if (_total > RowCap)
+ {
+
+ Showing the first @RowCap of @_total rows — cap protects the browser from megabyte-class
+ diffs. Inspect the remainder via the SQL sp_ComputeGenerationDiff directly.
+
+ }
+
+
+
+ | LogicalId | Change |
+
+
+ @foreach (var r in _visibleRows)
+ {
+
+ @r.LogicalId |
+
+ @switch (r.ChangeKind)
+ {
+ case "Added": @r.ChangeKind break;
+ case "Removed": @r.ChangeKind break;
+ case "Modified": @r.ChangeKind break;
+ default: @r.ChangeKind break;
+ }
+ |
+
+ }
+
+
+
+ }
+
+
+@code {
+ /// Default row-cap per section — matches task #156's acceptance criterion.
+ public const int DefaultRowCap = 1000;
+
+ [Parameter, EditorRequired] public string Title { get; set; } = string.Empty;
+ [Parameter] public string Description { get; set; } = string.Empty;
+ [Parameter, EditorRequired] public IReadOnlyList Rows { get; set; } = [];
+ [Parameter] public int RowCap { get; set; } = DefaultRowCap;
+
+ private int _total;
+ private int _added;
+ private int _removed;
+ private int _modified;
+ private List _visibleRows = [];
+
+ protected override void OnParametersSet()
+ {
+ _total = Rows.Count;
+ _added = 0; _removed = 0; _modified = 0;
+ foreach (var r in Rows)
+ {
+ switch (r.ChangeKind)
+ {
+ case "Added": _added++; break;
+ case "Removed": _removed++; break;
+ case "Modified": _modified++; break;
+ }
+ }
+ _visibleRows = _total > RowCap ? Rows.Take(RowCap).ToList() : Rows.ToList();
+ }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DiffViewer.razor b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DiffViewer.razor
index a9633c0..71b891d 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DiffViewer.razor
+++ b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DiffViewer.razor
@@ -28,36 +28,44 @@ else if (_rows.Count == 0)
}
else
{
-
- | Table | LogicalId | ChangeKind |
-
- @foreach (var r in _rows)
- {
-
- | @r.TableName |
- @r.LogicalId |
-
- @switch (r.ChangeKind)
- {
- case "Added": @r.ChangeKind break;
- case "Removed": @r.ChangeKind break;
- case "Modified": @r.ChangeKind break;
- default: @r.ChangeKind break;
- }
- |
-
- }
-
-
+
+ @_rows.Count row@(_rows.Count == 1 ? "" : "s") across @_sectionsWithChanges of @Sections.Count sections.
+ Each section is capped at @DiffSection.DefaultRowCap rows to keep the browser responsive on pathological drafts.
+
+
+ @foreach (var sec in Sections)
+ {
+
+ }
}
@code {
[Parameter] public string ClusterId { get; set; } = string.Empty;
[Parameter] public long GenerationId { get; set; }
+ ///
+ /// Ordered section definitions — each maps a TableName emitted by
+ /// sp_ComputeGenerationDiff to a human label + description. The proc currently
+ /// emits Namespace/DriverInstance/Equipment/Tag; UnsLine + NodeAcl entries render as
+ /// empty "no changes" cards until the proc is extended (tracked in tasks #196 + #156
+ /// follow-up). Six sections total matches the task #156 target.
+ ///
+ private static readonly IReadOnlyList Sections = new[]
+ {
+ new SectionDef("Namespace", "Namespaces", "OPC UA namespace URIs + enablement"),
+ new SectionDef("DriverInstance", "Driver instances","Per-cluster driver configuration rows"),
+ new SectionDef("Equipment", "Equipment", "UNS level-5 rows + identification fields"),
+ new SectionDef("Tag", "Tags", "Per-device tag definitions + poll-group binding"),
+ new SectionDef("UnsLine", "UNS structure", "Site / Area / Line hierarchy (proc-extension pending)"),
+ new SectionDef("NodeAcl", "ACLs", "LDAP-group → node-scope permission grants (proc-extension pending)"),
+ };
+
private List? _rows;
private string _fromLabel = "(empty)";
private string? _error;
+ private int _sectionsWithChanges;
protected override async Task OnParametersSetAsync()
{
@@ -67,7 +75,13 @@ else
var from = all.FirstOrDefault(g => g.Status == GenerationStatus.Published);
_fromLabel = from is null ? "(empty)" : $"gen {from.GenerationId}";
_rows = await GenerationSvc.ComputeDiffAsync(from?.GenerationId ?? 0, GenerationId, CancellationToken.None);
+ _sectionsWithChanges = Sections.Count(s => _rows.Any(r => r.TableName == s.TableName));
}
catch (Exception ex) { _error = ex.Message; }
}
+
+ private IReadOnlyList RowsFor(string tableName) =>
+ _rows?.Where(r => r.TableName == tableName).ToList() ?? [];
+
+ private sealed record SectionDef(string TableName, string Title, string Description);
}