From 3b8d05bf598b932b74cb474e268872f1657dbe9d Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Tue, 17 Feb 2026 09:48:51 -0500 Subject: [PATCH 1/9] [ZEPPELIN-6162] Implement revisions comparator for New UI Port the revision comparison feature from the legacy AngularJS UI to the new Angular 13 frontend. Users can now select two revisions and view paragraph-by-paragraph diffs with color-coded additions and deletions. --- .../message-data-type-map.interface.ts | 2 + .../interfaces/message-notebook.interface.ts | 7 + .../notebook/notebook.component.html | 6 +- .../workspace/notebook/notebook.module.ts | 6 +- .../revisions-comparator.component.html | 110 ++++++++- .../revisions-comparator.component.less | 140 ++++++++++++ .../revisions-comparator.component.ts | 212 +++++++++++++++++- 7 files changed, 471 insertions(+), 12 deletions(-) diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts index 25786552697..6c6088c73ae 100644 --- a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts +++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts @@ -34,6 +34,7 @@ import { NoteRename, NoteRevision, NoteRevisionForCompare, + NoteRevisionForCompareReceived, NoteRunningStatus, NoteUpdate, NoteUpdated, @@ -118,6 +119,7 @@ export interface MessageReceiveDataTypeMap { [OP.ANGULAR_OBJECT_UPDATE]: AngularObjectUpdate; [OP.ANGULAR_OBJECT_REMOVE]: AngularObjectRemove; [OP.PARAS_INFO]: ParasInfo; + [OP.NOTE_REVISION_FOR_COMPARE]: NoteRevisionForCompareReceived; } export interface MessageSendDataTypeMap { diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts index a07d52d372b..986aed0b910 100644 --- a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts +++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts @@ -125,6 +125,13 @@ export interface NoteRevisionForCompare { position: string; } +export interface NoteRevisionForCompareReceived { + noteId: string; + revisionId: string; + position: string; + note: Note['note']; +} + export interface CollaborativeModeStatus { status: boolean; users: string[]; diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html index ed0bddd2f2c..8541afc964a 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html @@ -56,7 +56,11 @@ [(activatedExtension)]="activatedExtension" [permissions]="permissions" > - +
-
-
-

Revisions comparator

+
+
+
+ + + + Revision name + Date + + + + + {{ revision.message }} + {{ formatRevisionDate(revision.time) }} + + + +
+ +
+ + + + compare with + + + +
+ +
+
+
+
+ {{ p.paragraph.id }} + ({{ p.paragraph.title }}) + added + deleted + differences + identical + {{ p.firstString }} +
+
+
+ Please select a revision +
+
+
+
+ +
+ + Revision: + {{ currentFirstRevisionLabel }} --> {{ currentSecondRevisionLabel }} + +
{{
+      currentParagraphDiffDisplay?.paragraph?.text
+    }}
+
{{
+      currentParagraphDiffDisplay?.paragraph?.text
+    }}
+

+    
+      
Nothing to display
+
- -
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less index 019b5ca53b5..90f44942b27 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less @@ -10,3 +10,143 @@ * limitations under the License. */ +.revisions-comparator { + padding: 10px 15px 15px; +} + +.commit-tree { + margin-bottom: 10px; + + ::ng-deep .ant-table-body { + overflow-y: auto !important; + } +} + +.cursor-hand { + cursor: pointer; +} + +.selected-revision { + background-color: rgba(24, 144, 255, 0.15) !important; +} + +.revisions-comparator-bar { + display: flex; + align-items: center; + gap: 8px; + padding-bottom: 12px; + + .revision-select { + width: 180px; + } + + .compare-label { + white-space: nowrap; + } +} + +.diff-panel { + border: 1px solid #d9d9d9; + border-radius: 4px; +} + +.paragraphs-div { + overflow: auto; + max-height: 35vh; +} + +.paragraph-item { + transition: background-color 200ms ease-out; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + + &:hover { + background-color: rgba(24, 144, 255, 0.08); + } + + &.paragraph-item-selected { + background-color: rgba(24, 144, 255, 0.15); + } +} + +.paragraph-item-heading { + padding: 8px 12px; +} + +.paragraph-id { + font-family: monospace; + font-size: 12px; + color: #595959; +} + +.paragraph-title { + padding: 0 5px; +} + +.paragraph-first-string { + display: block; + height: 1.8em; + overflow: hidden; + padding-top: 4px; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 12px; + color: #8c8c8c; +} + +.empty-paragraph-message { + font-size: 1.5em; + color: #8c8c8c; + text-align: center; + padding: 40px 0; +} + +.code-panel-col { + display: flex; + flex-direction: column; +} + +.code-panel-title { + font-size: 14px; + padding: 5px 0 8px; +} + +.code-panel { + flex: 1; + width: 100%; + min-height: 50vh; + max-height: 70vh; + overflow-y: auto; + border: 1px solid #d9d9d9; + border-radius: 4px; + padding: 8px; + margin: 0; + font-size: 13px; +} + +.empty-code-panel { + text-align: center; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + color: #8c8c8c; +} + +::ng-deep { + .color-green-row { + background-color: rgba(0, 226, 0, 0.15); + display: block; + color: #389e0d; + } + + .color-red-row { + background-color: rgba(226, 0, 0, 0.15); + display: block; + color: #cf1322; + } + + .color-black { + color: inherit; + } +} diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts index 1876b3cbbdb..33a4dae6371 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts @@ -10,16 +10,218 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import * as DiffMatchPatch from 'diff-match-patch'; +import { Subscription } from 'rxjs'; + +import { NoteRevisionForCompareReceived, OP, ParagraphItem, RevisionListItem } from '@zeppelin/sdk'; +import { MessageService } from '@zeppelin/services'; + +interface MergedParagraphDiff { + paragraph: ParagraphItem; + firstString: string; + type: 'added' | 'deleted' | 'compared'; + diff?: SafeHtml; + identical?: boolean; +} @Component({ selector: 'zeppelin-notebook-revisions-comparator', templateUrl: './revisions-comparator.component.html', styleUrls: ['./revisions-comparator.component.less'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [DatePipe] }) -export class NotebookRevisionsComparatorComponent implements OnInit { - constructor() {} +export class NotebookRevisionsComparatorComponent implements OnInit, OnDestroy { + @Input() noteRevisions: RevisionListItem[] = []; + @Input() noteId!: string; + + firstNoteRevisionForCompare: NoteRevisionForCompareReceived | null = null; + secondNoteRevisionForCompare: NoteRevisionForCompareReceived | null = null; + currentFirstRevisionLabel = 'Choose...'; + currentSecondRevisionLabel = 'Choose...'; + mergeNoteRevisionsForCompare: MergedParagraphDiff[] = []; + currentParagraphDiffDisplay: MergedParagraphDiff | null = null; + selectedFirstRevisionId: string | null = null; + selectedSecondRevisionId: string | null = null; + + private subscription: Subscription | null = null; + private dmp = new DiffMatchPatch(); + + get sortedRevisions(): RevisionListItem[] { + return [...this.noteRevisions].sort((a, b) => (b.time || 0) - (a.time || 0)); + } + + constructor( + private messageService: MessageService, + private cdr: ChangeDetectorRef, + private datePipe: DatePipe, + private sanitizer: DomSanitizer + ) {} + + ngOnInit(): void { + this.subscription = this.messageService + .receive(OP.NOTE_REVISION_FOR_COMPARE) + .subscribe((data: NoteRevisionForCompareReceived) => { + if (data.note && data.position) { + if (data.position === 'first') { + this.firstNoteRevisionForCompare = data; + } else { + this.secondNoteRevisionForCompare = data; + } + + if ( + this.firstNoteRevisionForCompare !== null && + this.secondNoteRevisionForCompare !== null && + this.firstNoteRevisionForCompare.revisionId !== this.secondNoteRevisionForCompare.revisionId + ) { + this.compareRevisions(); + } + this.cdr.markForCheck(); + } + }); + } + + getNoteRevisionForReview(revision: RevisionListItem, position: 'first' | 'second'): void { + if (!revision) { + return; + } + if (position === 'first') { + this.currentFirstRevisionLabel = revision.message; + this.selectedFirstRevisionId = revision.id; + } else { + this.currentSecondRevisionLabel = revision.message; + this.selectedSecondRevisionId = revision.id; + } + this.messageService.noteRevisionForCompare(this.noteId, revision.id, position); + } + + onFirstRevisionSelect(revisionId: string): void { + const revision = this.noteRevisions.find(r => r.id === revisionId); + if (revision) { + this.getNoteRevisionForReview(revision, 'first'); + } + } + + onSecondRevisionSelect(revisionId: string): void { + const revision = this.noteRevisions.find(r => r.id === revisionId); + if (revision) { + this.getNoteRevisionForReview(revision, 'second'); + } + } + + onRevisionRowClick(index: number): void { + const sorted = this.sortedRevisions; + if (index < sorted.length - 1) { + this.getNoteRevisionForReview(sorted[index + 1], 'first'); + this.getNoteRevisionForReview(sorted[index], 'second'); + } + } + + compareRevisions(): void { + if (!this.firstNoteRevisionForCompare || !this.secondNoteRevisionForCompare) { + return; + } + const paragraphs1 = this.firstNoteRevisionForCompare.note?.paragraphs || []; + const paragraphs2 = this.secondNoteRevisionForCompare.note?.paragraphs || []; + const merge: MergedParagraphDiff[] = []; + + for (const p1 of paragraphs1) { + const p2 = paragraphs2.find((p: ParagraphItem) => p.id === p1.id) || null; + if (p2 === null) { + merge.push({ + paragraph: p1, + firstString: (p1.text || '').split('\n')[0], + type: 'deleted' + }); + } else { + const text1 = p1.text || ''; + const text2 = p2.text || ''; + const diffHtml = this.buildLineDiffHtml(text1, text2); + merge.push({ + paragraph: p1, + diff: diffHtml.html, + identical: diffHtml.identical, + firstString: (p1.text || '').split('\n')[0], + type: 'compared' + }); + } + } + + for (const p2 of paragraphs2) { + const p1 = paragraphs1.find((p: ParagraphItem) => p.id === p2.id) || null; + if (p1 === null) { + merge.push({ + paragraph: p2, + firstString: (p2.text || '').split('\n')[0], + type: 'added' + }); + } + } + + merge.sort((a, b) => { + const order = { added: 0, deleted: 1, compared: 2 }; + return order[a.type] - order[b.type]; + }); + + this.mergeNoteRevisionsForCompare = merge; + + if (this.currentParagraphDiffDisplay !== null) { + this.changeCurrentParagraphDiffDisplay(this.currentParagraphDiffDisplay.paragraph.id); + } + } + + changeCurrentParagraphDiffDisplay(paragraphId: string): void { + const found = this.mergeNoteRevisionsForCompare.find(p => p.paragraph.id === paragraphId); + this.currentParagraphDiffDisplay = found || null; + } + + formatRevisionDate(time: number | undefined): string { + if (!time) { + return ''; + } + return this.datePipe.transform(time * 1000, 'MMMM d yyyy, h:mm:ss a') || ''; + } + + private buildLineDiffHtml(text1: string, text2: string): { html: SafeHtml; identical: boolean } { + const a = this.dmp.diff_linesToChars_(text1, text2); + const diffs = this.dmp.diff_main(a.chars1, a.chars2, false); + this.dmp.diff_charsToLines_(diffs, a.lineArray); + + let identical = true; + let html = ''; + + for (const [op, text] of diffs) { + let str = text; + if (str.length > 0 && str[str.length - 1] !== '\n') { + str = `${str}\n`; + } + const escaped = this.escapeHtml(str); + if (op === DiffMatchPatch.DIFF_INSERT) { + html += `${escaped}`; + identical = false; + } else if (op === DiffMatchPatch.DIFF_DELETE) { + html += `${escaped}`; + identical = false; + } else { + html += `${escaped}`; + } + } + + return { html: this.sanitizer.bypassSecurityTrustHtml(html), identical }; + } + + private escapeHtml(text: string): string { + const div = document.createElement('div'); + div.appendChild(document.createTextNode(text)); + return div.innerHTML; + } - ngOnInit() {} + ngOnDestroy(): void { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } } From 3ee897c159cb4301dbfe6b1aeb709601d2179990 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Tue, 17 Feb 2026 10:41:47 -0500 Subject: [PATCH 2/9] use colors from mixin instead of hard-coded --- .../revisions-comparator.component.less | 251 +++++++++--------- 1 file changed, 127 insertions(+), 124 deletions(-) diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less index 90f44942b27..48020caac1c 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less @@ -9,144 +9,147 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "theme-mixin"; -.revisions-comparator { - padding: 10px 15px 15px; -} +.themeMixin({ + .revisions-comparator { + padding: 10px 15px 15px; + } + + .commit-tree { + margin-bottom: 10px; -.commit-tree { - margin-bottom: 10px; + ::ng-deep .ant-table-body { + overflow-y: auto !important; + } + } - ::ng-deep .ant-table-body { - overflow-y: auto !important; + .cursor-hand { + cursor: pointer; } -} -.cursor-hand { - cursor: pointer; -} + .selected-revision { + background-color: fade(@primary-6, 15%) !important; + } -.selected-revision { - background-color: rgba(24, 144, 255, 0.15) !important; -} + .revisions-comparator-bar { + display: flex; + align-items: center; + gap: 8px; + padding-bottom: 12px; -.revisions-comparator-bar { - display: flex; - align-items: center; - gap: 8px; - padding-bottom: 12px; + .revision-select { + width: 180px; + } - .revision-select { - width: 180px; + .compare-label { + white-space: nowrap; + } } - .compare-label { - white-space: nowrap; + .diff-panel { + border: 1px solid @border-color-base; + border-radius: 4px; } -} - -.diff-panel { - border: 1px solid #d9d9d9; - border-radius: 4px; -} - -.paragraphs-div { - overflow: auto; - max-height: 35vh; -} - -.paragraph-item { - transition: background-color 200ms ease-out; - border-bottom: 1px solid #f0f0f0; - cursor: pointer; - - &:hover { - background-color: rgba(24, 144, 255, 0.08); - } - - &.paragraph-item-selected { - background-color: rgba(24, 144, 255, 0.15); - } -} - -.paragraph-item-heading { - padding: 8px 12px; -} - -.paragraph-id { - font-family: monospace; - font-size: 12px; - color: #595959; -} - -.paragraph-title { - padding: 0 5px; -} - -.paragraph-first-string { - display: block; - height: 1.8em; - overflow: hidden; - padding-top: 4px; - white-space: nowrap; - text-overflow: ellipsis; - font-size: 12px; - color: #8c8c8c; -} - -.empty-paragraph-message { - font-size: 1.5em; - color: #8c8c8c; - text-align: center; - padding: 40px 0; -} - -.code-panel-col { - display: flex; - flex-direction: column; -} - -.code-panel-title { - font-size: 14px; - padding: 5px 0 8px; -} - -.code-panel { - flex: 1; - width: 100%; - min-height: 50vh; - max-height: 70vh; - overflow-y: auto; - border: 1px solid #d9d9d9; - border-radius: 4px; - padding: 8px; - margin: 0; - font-size: 13px; -} - -.empty-code-panel { - text-align: center; - display: flex; - align-items: center; - justify-content: center; - font-size: 24px; - color: #8c8c8c; -} - -::ng-deep { - .color-green-row { - background-color: rgba(0, 226, 0, 0.15); - display: block; - color: #389e0d; + + .paragraphs-div { + overflow: auto; + max-height: 35vh; + } + + .paragraph-item { + transition: background-color 200ms ease-out; + border-bottom: 1px solid @border-color-split; + cursor: pointer; + + &:hover { + background-color: fade(@primary-6, 8%); + } + + &.paragraph-item-selected { + background-color: fade(@primary-6, 15%); + } + } + + .paragraph-item-heading { + padding: 8px 12px; + } + + .paragraph-id { + font-family: monospace; + font-size: 12px; + color: @text-color-secondary; + } + + .paragraph-title { + padding: 0 5px; } - .color-red-row { - background-color: rgba(226, 0, 0, 0.15); + .paragraph-first-string { display: block; - color: #cf1322; + height: 1.8em; + overflow: hidden; + padding-top: 4px; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 12px; + color: @text-color-secondary; + } + + .empty-paragraph-message { + font-size: 1.5em; + color: @text-color-secondary; + text-align: center; + padding: 40px 0; + } + + .code-panel-col { + display: flex; + flex-direction: column; + } + + .code-panel-title { + font-size: 14px; + padding: 5px 0 8px; + } + + .code-panel { + flex: 1; + width: 100%; + min-height: 50vh; + max-height: 70vh; + overflow-y: auto; + border: 1px solid @border-color-base; + border-radius: 4px; + padding: 8px; + margin: 0; + font-size: 13px; + } + + .empty-code-panel { + text-align: center; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + color: @text-color-secondary; } - .color-black { - color: inherit; + ::ng-deep { + .color-green-row { + background-color: fade(@green-6, 15%); + display: block; + color: @green-6; + } + + .color-red-row { + background-color: fade(@red-6, 15%); + display: block; + color: @red-6; + } + + .color-black { + color: inherit; + } } -} +}); From e78cfc39b0e43e150d3d141095398be26c6d12a4 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Sun, 22 Feb 2026 21:01:35 -0500 Subject: [PATCH 3/9] ZEPPELIN-6162: Destructure object --- .../revisions-comparator/revisions-comparator.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts index 33a4dae6371..ae8b9704278 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts @@ -186,9 +186,9 @@ export class NotebookRevisionsComparatorComponent implements OnInit, OnDestroy { } private buildLineDiffHtml(text1: string, text2: string): { html: SafeHtml; identical: boolean } { - const a = this.dmp.diff_linesToChars_(text1, text2); - const diffs = this.dmp.diff_main(a.chars1, a.chars2, false); - this.dmp.diff_charsToLines_(diffs, a.lineArray); + const { chars1, chars2, lineArray } = this.dmp.diff_linesToChars_(text1, text2); + const diffs = this.dmp.diff_main(chars1, chars2, false); + this.dmp.diff_charsToLines_(diffs, lineArray); let identical = true; let html = ''; From 517beb6b982dcc3e3fc9e005301dd3096fd356a8 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Sun, 22 Feb 2026 21:05:30 -0500 Subject: [PATCH 4/9] ZEPPELIN-6162: fix flex (date) --- .../revisions-comparator/revisions-comparator.component.less | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less index 48020caac1c..8eae3f0cc4b 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less @@ -37,9 +37,12 @@ align-items: center; gap: 8px; padding-bottom: 12px; + flex-wrap: wrap; .revision-select { - width: 180px; + flex: 1; + min-width: 100px; + display: block; } .compare-label { From 1acbdea02f6fcbe66b0aa77cc17bbab33415c706 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Sun, 22 Feb 2026 21:21:14 -0500 Subject: [PATCH 5/9] ZEPPELIN-6162: Fix sorting based on date --- .../revisions-comparator.component.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts index ae8b9704278..ea6fcb78a8d 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts @@ -124,8 +124,8 @@ export class NotebookRevisionsComparatorComponent implements OnInit, OnDestroy { if (!this.firstNoteRevisionForCompare || !this.secondNoteRevisionForCompare) { return; } - const paragraphs1 = this.firstNoteRevisionForCompare.note?.paragraphs || []; - const paragraphs2 = this.secondNoteRevisionForCompare.note?.paragraphs || []; + const paragraphs1 = this.secondNoteRevisionForCompare.note?.paragraphs || []; + const paragraphs2 = this.firstNoteRevisionForCompare.note?.paragraphs || []; const merge: MergedParagraphDiff[] = []; for (const p1 of paragraphs1) { @@ -134,7 +134,7 @@ export class NotebookRevisionsComparatorComponent implements OnInit, OnDestroy { merge.push({ paragraph: p1, firstString: (p1.text || '').split('\n')[0], - type: 'deleted' + type: 'added' }); } else { const text1 = p1.text || ''; @@ -156,16 +156,11 @@ export class NotebookRevisionsComparatorComponent implements OnInit, OnDestroy { merge.push({ paragraph: p2, firstString: (p2.text || '').split('\n')[0], - type: 'added' + type: 'deleted' }); } } - merge.sort((a, b) => { - const order = { added: 0, deleted: 1, compared: 2 }; - return order[a.type] - order[b.type]; - }); - this.mergeNoteRevisionsForCompare = merge; if (this.currentParagraphDiffDisplay !== null) { @@ -210,7 +205,7 @@ export class NotebookRevisionsComparatorComponent implements OnInit, OnDestroy { } } - return { html: this.sanitizer.bypassSecurityTrustHtml(html), identical }; + return { html, identical }; } private escapeHtml(text: string): string { From 7e46b78347f12702a452b49607f0fbbe4f0c1079 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Sun, 22 Feb 2026 22:19:55 -0500 Subject: [PATCH 6/9] ZEPPELIN-6162: Fix dark theme --- .../src/styles/theme/dark-theme-overrides.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/zeppelin-web-angular/src/styles/theme/dark-theme-overrides.css b/zeppelin-web-angular/src/styles/theme/dark-theme-overrides.css index 6b762b23152..a2074f077d8 100644 --- a/zeppelin-web-angular/src/styles/theme/dark-theme-overrides.css +++ b/zeppelin-web-angular/src/styles/theme/dark-theme-overrides.css @@ -166,6 +166,17 @@ html.dark .ant-menu-submenu-title:hover { color: rgba(255, 255, 255, 0.95) !important; } +html.dark .ant-select-selector { + background-color: #262626 !important; + border-color: #434343 !important; + color: rgba(255, 255, 255, 0.85) !important; +} + +html.dark .ant-select-selector:hover, +html.dark .ant-select-selector:focus { + border-color: #177ddc !important; +} + html.dark .ant-dropdown-menu-item-selected, html.dark .ant-dropdown-menu-submenu-title-selected, html.dark .ant-dropdown-menu-item-selected > a, From 7e7923515ecff64d7617c2b7071c5f885830d70c Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Sun, 22 Feb 2026 22:33:36 -0500 Subject: [PATCH 7/9] fix background color --- .../src/styles/theme/dark-theme-overrides.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zeppelin-web-angular/src/styles/theme/dark-theme-overrides.css b/zeppelin-web-angular/src/styles/theme/dark-theme-overrides.css index a2074f077d8..df1106ec293 100644 --- a/zeppelin-web-angular/src/styles/theme/dark-theme-overrides.css +++ b/zeppelin-web-angular/src/styles/theme/dark-theme-overrides.css @@ -177,6 +177,10 @@ html.dark .ant-select-selector:focus { border-color: #177ddc !important; } +html.dark .ant-select-item-option-active:not(html.dark .ant-select-item-option-disabled) { + background-color: #262626 !important; +} + html.dark .ant-dropdown-menu-item-selected, html.dark .ant-dropdown-menu-submenu-title-selected, html.dark .ant-dropdown-menu-item-selected > a, From 27505a98a1767a3d1e21d1fe232b7012bce5f964 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Wed, 25 Feb 2026 13:52:27 -0500 Subject: [PATCH 8/9] ZEPPELIN-6162: fix variable names --- .../revisions-comparator.component.html | 2 +- .../revisions-comparator.component.ts | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.html b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.html index 77619045e71..ae90c03c630 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.html +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.html @@ -74,7 +74,7 @@
p.id === p1.id) || null; + for (const p1 of baseParagraphs) { + const p2 = compareParagraphs.find((p: ParagraphItem) => p.id === p1.id) || null; if (p2 === null) { - merge.push({ + paragraphDiffs.push({ paragraph: p1, firstString: (p1.text || '').split('\n')[0], type: 'added' @@ -140,7 +140,7 @@ export class NotebookRevisionsComparatorComponent implements OnInit, OnDestroy { const text1 = p1.text || ''; const text2 = p2.text || ''; const diffHtml = this.buildLineDiffHtml(text1, text2); - merge.push({ + paragraphDiffs.push({ paragraph: p1, diff: diffHtml.html, identical: diffHtml.identical, @@ -150,10 +150,10 @@ export class NotebookRevisionsComparatorComponent implements OnInit, OnDestroy { } } - for (const p2 of paragraphs2) { - const p1 = paragraphs1.find((p: ParagraphItem) => p.id === p2.id) || null; + for (const p2 of compareParagraphs) { + const p1 = baseParagraphs.find((p: ParagraphItem) => p.id === p2.id) || null; if (p1 === null) { - merge.push({ + paragraphDiffs.push({ paragraph: p2, firstString: (p2.text || '').split('\n')[0], type: 'deleted' @@ -161,7 +161,7 @@ export class NotebookRevisionsComparatorComponent implements OnInit, OnDestroy { } } - this.mergeNoteRevisionsForCompare = merge; + this.mergeNoteRevisionsDiff = paragraphDiffs; if (this.currentParagraphDiffDisplay !== null) { this.changeCurrentParagraphDiffDisplay(this.currentParagraphDiffDisplay.paragraph.id); @@ -169,7 +169,7 @@ export class NotebookRevisionsComparatorComponent implements OnInit, OnDestroy { } changeCurrentParagraphDiffDisplay(paragraphId: string): void { - const found = this.mergeNoteRevisionsForCompare.find(p => p.paragraph.id === paragraphId); + const found = this.mergeNoteRevisionsDiff.find(p => p.paragraph.id === paragraphId); this.currentParagraphDiffDisplay = found || null; } From 67980fc6634573f9a98214ec15d55069b4430c7c Mon Sep 17 00:00:00 2001 From: YONGJAE LEE Date: Fri, 20 Feb 2026 22:04:34 +0900 Subject: [PATCH 9/9] fix XSS in revisions comparator --- .../revisions-comparator.component.html | 11 ++--- .../revisions-comparator.component.ts | 41 ++++++++----------- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.html b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.html index ae90c03c630..17dda5a0ffd 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.html +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.html @@ -107,11 +107,12 @@
{{
       currentParagraphDiffDisplay?.paragraph?.text
     }}
-

+    
{{ seg.text }}
       
Nothing to display
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts index cfdc568090b..3b45e77d44d 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts @@ -12,18 +12,22 @@ import { DatePipe } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import * as DiffMatchPatch from 'diff-match-patch'; import { Subscription } from 'rxjs'; import { NoteRevisionForCompareReceived, OP, ParagraphItem, RevisionListItem } from '@zeppelin/sdk'; import { MessageService } from '@zeppelin/services'; +interface DiffSegment { + type: 'insert' | 'delete' | 'equal'; + text: string; +} + interface MergedParagraphDiff { paragraph: ParagraphItem; firstString: string; type: 'added' | 'deleted' | 'compared'; - diff?: SafeHtml; + segments?: DiffSegment[]; identical?: boolean; } @@ -46,7 +50,6 @@ export class NotebookRevisionsComparatorComponent implements OnInit, OnDestroy { currentParagraphDiffDisplay: MergedParagraphDiff | null = null; selectedFirstRevisionId: string | null = null; selectedSecondRevisionId: string | null = null; - private subscription: Subscription | null = null; private dmp = new DiffMatchPatch(); @@ -57,8 +60,7 @@ export class NotebookRevisionsComparatorComponent implements OnInit, OnDestroy { constructor( private messageService: MessageService, private cdr: ChangeDetectorRef, - private datePipe: DatePipe, - private sanitizer: DomSanitizer + private datePipe: DatePipe ) {} ngOnInit(): void { @@ -139,11 +141,11 @@ export class NotebookRevisionsComparatorComponent implements OnInit, OnDestroy { } else { const text1 = p1.text || ''; const text2 = p2.text || ''; - const diffHtml = this.buildLineDiffHtml(text1, text2); + const diffResult = this.buildLineDiff(text1, text2); paragraphDiffs.push({ paragraph: p1, - diff: diffHtml.html, - identical: diffHtml.identical, + segments: diffResult.segments, + identical: diffResult.identical, firstString: (p1.text || '').split('\n')[0], type: 'compared' }); @@ -180,38 +182,27 @@ export class NotebookRevisionsComparatorComponent implements OnInit, OnDestroy { return this.datePipe.transform(time * 1000, 'MMMM d yyyy, h:mm:ss a') || ''; } - private buildLineDiffHtml(text1: string, text2: string): { html: SafeHtml; identical: boolean } { + private buildLineDiff(text1: string, text2: string): { segments: DiffSegment[]; identical: boolean } { const { chars1, chars2, lineArray } = this.dmp.diff_linesToChars_(text1, text2); const diffs = this.dmp.diff_main(chars1, chars2, false); this.dmp.diff_charsToLines_(diffs, lineArray); let identical = true; - let html = ''; + const segments: DiffSegment[] = []; for (const [op, text] of diffs) { - let str = text; - if (str.length > 0 && str[str.length - 1] !== '\n') { - str = `${str}\n`; - } - const escaped = this.escapeHtml(str); if (op === DiffMatchPatch.DIFF_INSERT) { - html += `${escaped}`; + segments.push({ type: 'insert', text }); identical = false; } else if (op === DiffMatchPatch.DIFF_DELETE) { - html += `${escaped}`; + segments.push({ type: 'delete', text }); identical = false; } else { - html += `${escaped}`; + segments.push({ type: 'equal', text }); } } - return { html, identical }; - } - - private escapeHtml(text: string): string { - const div = document.createElement('div'); - div.appendChild(document.createTextNode(text)); - return div.innerHTML; + return { segments, identical }; } ngOnDestroy(): void {