@page "/clusters/{ClusterId}/draft/{GenerationId:long}/diff" @using ZB.MOM.WW.OtOpcUa.Admin.Services @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @using ZB.MOM.WW.OtOpcUa.Configuration.Enums @inject GenerationService GenerationSvc

Draft diff

Cluster @ClusterId — from last published (@(_fromLabel)) → to draft @GenerationId
Back to editor
@if (_rows is null) {

Computing diff…

} else if (_error is not null) {
@_error
} else if (_rows.Count == 0) {

No differences — draft is structurally identical to the last published generation.

} else {

@_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() { try { var all = await GenerationSvc.ListRecentAsync(ClusterId, 50, CancellationToken.None); 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); }