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. *@ + +
+
+
+ @Title + @Description +
+
+ @if (_added > 0) { +@_added } + @if (_removed > 0) { −@_removed } + @if (_modified > 0) { ~@_modified } + @if (_total == 0) { no changes } +
+
+ @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. +
+ } +
+ + + + + + @foreach (var r in _visibleRows) + { + + + + + } + +
LogicalIdChange
@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 { - - - - @foreach (var r in _rows) - { - - - - - - } - -
TableLogicalIdChangeKind
@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); }