Skip to content

Commit d1ff571

Browse files
committed
NIFI-13125 - Flow Version Diff View
1 parent efb8a48 commit d1ff571

File tree

9 files changed

+815
-9
lines changed

9 files changed

+815
-9
lines changed

nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/registry.service.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import { Injectable, inject } from '@angular/core';
1919
import { Observable } from 'rxjs';
2020
import { HttpClient, HttpParams } from '@angular/common/http';
21-
import { ImportFromRegistryRequest } from '../state/flow';
21+
import { FlowComparisonEntity, ImportFromRegistryRequest } from '../state/flow';
2222

2323
@Injectable({ providedIn: 'root' })
2424
export class RegistryService {
@@ -63,6 +63,31 @@ export class RegistryService {
6363
);
6464
}
6565

66+
getFlowDiff(
67+
registryId: string,
68+
bucketId: string,
69+
flowId: string,
70+
versionA: string,
71+
versionB: string,
72+
branch?: string | null
73+
): Observable<FlowComparisonEntity> {
74+
const encodedRegistryId = encodeURIComponent(registryId);
75+
const encodedBucketId = encodeURIComponent(bucketId);
76+
const encodedFlowId = encodeURIComponent(flowId);
77+
const encodedVersionA = encodeURIComponent(versionA);
78+
const encodedVersionB = encodeURIComponent(versionB);
79+
80+
if (branch) {
81+
const encodedBranch = encodeURIComponent(branch);
82+
const diffEndpoint = `${RegistryService.API}/flow/registries/${encodedRegistryId}/branches/${encodedBranch}/buckets/${encodedBucketId}/flows/${encodedFlowId}/${encodedVersionA}/diff/branches/${encodedBranch}/buckets/${encodedBucketId}/flows/${encodedFlowId}/${encodedVersionB}`;
83+
return this.httpClient.get(diffEndpoint) as Observable<FlowComparisonEntity>;
84+
}
85+
86+
let params: HttpParams = new HttpParams();
87+
const legacyEndpoint = `${RegistryService.API}/flow/registries/${encodedRegistryId}/buckets/${encodedBucketId}/flows/${encodedFlowId}/diff/${encodedVersionA}/${encodedVersionB}`;
88+
return this.httpClient.get(legacyEndpoint, { params }) as Observable<FlowComparisonEntity>;
89+
}
90+
6691
importFromRegistry(processGroupId: string, request: ImportFromRegistryRequest): Observable<any> {
6792
return this.httpClient.post(
6893
`${RegistryService.API}/process-groups/${processGroupId}/process-groups`,

nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/change-version-dialog/change-version-dialog.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,24 @@ <h2 mat-dialog-title>
7878
(matSortChange)="sortData($event)"
7979
[matSortActive]="sort.active"
8080
[matSortDirection]="sort.direction">
81+
<!-- Actions Column -->
82+
<ng-container matColumnDef="actions">
83+
<th mat-header-cell *matHeaderCellDef></th>
84+
<td mat-cell *matCellDef="let item">
85+
<button
86+
mat-icon-button
87+
type="button"
88+
(click)="openFlowDiff(item, $event)"
89+
aria-label="View flow version diff"
90+
nifiTooltip
91+
[tooltipComponentType]="TextTip"
92+
tooltipInputData="View flow version diff"
93+
data-qa="view-flow-diff-button">
94+
<i class="fa fa-exchange"></i>
95+
</button>
96+
</td>
97+
</ng-container>
98+
8199
<!-- Current Version Column -->
82100
<ng-container matColumnDef="current">
83101
<th mat-header-cell *matHeaderCellDef></th>

nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/change-version-dialog/change-version-dialog.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222

2323
.listing-table {
2424
table {
25+
.mat-column-actions {
26+
width: 54px;
27+
}
28+
2529
.mat-column-current {
2630
width: 42px;
2731
}

nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/change-version-dialog/change-version-dialog.spec.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { By } from '@angular/platform-browser';
2121
import { ChangeVersionDialog } from './change-version-dialog';
2222
import { ChangeVersionDialogRequest } from '../../../../../state/flow';
2323
import { VersionedFlowSnapshotMetadata } from '../../../../../../../state/shared';
24-
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
24+
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
2525
import { provideMockStore } from '@ngrx/store/testing';
2626
import { initialState } from '../../../../../state/flow/flow.reducer';
2727
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@@ -33,6 +33,7 @@ import { currentUserFeatureKey } from '../../../../../../../state/current-user';
3333
import { canvasFeatureKey } from '../../../../../state';
3434
import { flowFeatureKey } from '../../../../../state/flow';
3535
import { Sort } from '@angular/material/sort';
36+
import { FlowDiffDialog } from '../flow-diff-dialog/flow-diff-dialog';
3637

3738
interface SetupOptions {
3839
dialogData?: ChangeVersionDialogRequest;
@@ -92,6 +93,7 @@ describe('ChangeVersionDialog', () => {
9293
async function setup(options: SetupOptions = {}) {
9394
const dialogData = options.dialogData || createMockDialogData();
9495
const timeOffset = options.timeOffset || 0;
96+
const matDialogOpen = jest.fn();
9597

9698
await TestBed.configureTestingModule({
9799
imports: [ChangeVersionDialog, NoopAnimationsModule],
@@ -112,15 +114,21 @@ describe('ChangeVersionDialog', () => {
112114
}
113115
]
114116
}),
115-
{ provide: MatDialogRef, useValue: null }
117+
{ provide: MatDialogRef, useValue: null },
118+
{
119+
provide: MatDialog,
120+
useValue: {
121+
open: matDialogOpen
122+
}
123+
}
116124
]
117125
}).compileComponents();
118126

119127
const fixture = TestBed.createComponent(ChangeVersionDialog);
120128
const component = fixture.componentInstance;
121129
fixture.detectChanges();
122130

123-
return { fixture, component, dialogData };
131+
return { fixture, component, dialogData, matDialogOpen };
124132
}
125133

126134
beforeEach(() => {
@@ -133,6 +141,12 @@ describe('ChangeVersionDialog', () => {
133141
expect(component).toBeTruthy();
134142
});
135143

144+
it('should include flow diff action column', async () => {
145+
const { component } = await setup();
146+
147+
expect(component.displayedColumns).toContain('actions');
148+
});
149+
136150
describe('Component Initialization', () => {
137151
it('should initialize with sorted flow versions', async () => {
138152
const { component } = await setup();
@@ -214,6 +228,34 @@ describe('ChangeVersionDialog', () => {
214228
});
215229
});
216230

231+
describe('Flow Diff', () => {
232+
it('should open the flow diff dialog when the action is triggered', async () => {
233+
const { fixture, matDialogOpen, dialogData } = await setup();
234+
const diffButton = fixture.debugElement.query(By.css('[data-qa="view-flow-diff-button"]'));
235+
const mockEvent = {
236+
preventDefault: jest.fn(),
237+
stopPropagation: jest.fn()
238+
} as unknown as MouseEvent;
239+
240+
diffButton.triggerEventHandler('click', mockEvent);
241+
242+
expect(mockEvent.preventDefault).toHaveBeenCalled();
243+
expect(mockEvent.stopPropagation).toHaveBeenCalled();
244+
const icon = diffButton.query(By.css('i'));
245+
expect(icon.nativeElement.classList).toContain('fa-exchange');
246+
expect(matDialogOpen).toHaveBeenCalledWith(
247+
FlowDiffDialog,
248+
expect.objectContaining({
249+
data: expect.objectContaining({
250+
versionControlInformation: dialogData.versionControlInformation,
251+
currentVersion: dialogData.versionControlInformation.version,
252+
selectedVersion: dialogData.versions[0].versionedFlowSnapshotMetadata.version
253+
})
254+
})
255+
);
256+
});
257+
});
258+
217259
describe('Selection Validation', () => {
218260
it('should be invalid when no version is selected', async () => {
219261
const { component } = await setup();

nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/change-version-dialog/change-version-dialog.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@
1616
*/
1717

1818
import { Component, EventEmitter, Output, inject } from '@angular/core';
19-
import { MatButton } from '@angular/material/button';
19+
import { MatButton, MatIconButton } from '@angular/material/button';
2020
import { MatCell, MatCellDef, MatColumnDef, MatTableDataSource, MatTableModule } from '@angular/material/table';
21-
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
21+
import { MAT_DIALOG_DATA, MatDialog, MatDialogModule } from '@angular/material/dialog';
2222
import { MatSortModule, Sort } from '@angular/material/sort';
2323
import { VersionedFlowSnapshotMetadata } from '../../../../../../../state/shared';
2424
import { ChangeVersionDialogRequest, VersionControlInformation } from '../../../../../state/flow';
2525
import { Store } from '@ngrx/store';
2626
import { CanvasState } from '../../../../../state';
2727
import { selectTimeOffset } from '../../../../../../../state/flow-configuration/flow-configuration.selectors';
28-
import { NiFiCommon, CloseOnEscapeDialog, NifiTooltipDirective, TextTip } from '@nifi/shared';
28+
import { NiFiCommon, CloseOnEscapeDialog, NifiTooltipDirective, TextTip, LARGE_DIALOG } from '@nifi/shared';
29+
import { FlowDiffDialog, FlowDiffDialogData } from '../flow-diff-dialog/flow-diff-dialog';
2930

3031
@Component({
3132
selector: 'change-version-dialog',
@@ -37,7 +38,8 @@ import { NiFiCommon, CloseOnEscapeDialog, NifiTooltipDirective, TextTip } from '
3738
MatDialogModule,
3839
MatSortModule,
3940
MatTableModule,
40-
NifiTooltipDirective
41+
NifiTooltipDirective,
42+
MatIconButton
4143
],
4244
templateUrl: './change-version-dialog.html',
4345
styleUrl: './change-version-dialog.scss'
@@ -47,16 +49,18 @@ export class ChangeVersionDialog extends CloseOnEscapeDialog {
4749
private nifiCommon = inject(NiFiCommon);
4850
private store = inject<Store<CanvasState>>(Store);
4951

50-
displayedColumns: string[] = ['current', 'version', 'created', 'comments'];
52+
displayedColumns: string[] = ['actions', 'current', 'version', 'created', 'comments'];
5153
dataSource: MatTableDataSource<VersionedFlowSnapshotMetadata> =
5254
new MatTableDataSource<VersionedFlowSnapshotMetadata>();
5355
selectedFlowVersion: VersionedFlowSnapshotMetadata | null = null;
56+
private allFlowVersions: VersionedFlowSnapshotMetadata[] = [];
5457
sort: Sort = {
5558
active: 'created',
5659
direction: 'desc'
5760
};
5861
versionControlInformation: VersionControlInformation;
5962
private timeOffset = this.store.selectSignal(selectTimeOffset);
63+
private dialog = inject(MatDialog);
6064

6165
@Output() changeVersion: EventEmitter<VersionedFlowSnapshotMetadata> =
6266
new EventEmitter<VersionedFlowSnapshotMetadata>();
@@ -66,6 +70,7 @@ export class ChangeVersionDialog extends CloseOnEscapeDialog {
6670
const dialogRequest = this.dialogRequest;
6771

6872
const flowVersions = dialogRequest.versions.map((entity) => entity.versionedFlowSnapshotMetadata);
73+
this.allFlowVersions = flowVersions;
6974
const sortedFlowVersions = this.sortVersions(flowVersions, this.sort);
7075
this.selectedFlowVersion = sortedFlowVersions[0];
7176
this.dataSource.data = sortedFlowVersions;
@@ -151,5 +156,27 @@ export class ChangeVersionDialog extends CloseOnEscapeDialog {
151156
return flowVersion.version === this.versionControlInformation.version;
152157
}
153158

159+
openFlowDiff(flowVersion: VersionedFlowSnapshotMetadata, event: MouseEvent) {
160+
event.preventDefault();
161+
event.stopPropagation();
162+
163+
if (!this.versionControlInformation) {
164+
return;
165+
}
166+
167+
const dialogData: FlowDiffDialogData = {
168+
versionControlInformation: this.versionControlInformation,
169+
versions: this.allFlowVersions,
170+
currentVersion: this.versionControlInformation.version,
171+
selectedVersion: flowVersion.version
172+
};
173+
174+
this.dialog.open(FlowDiffDialog, {
175+
...LARGE_DIALOG,
176+
data: dialogData,
177+
autoFocus: false
178+
});
179+
}
180+
154181
protected readonly TextTip = TextTip;
155182
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<!--
2+
~ Licensed to the Apache Software Foundation (ASF) under one or more
3+
~ contributor license agreements. See the NOTICE file distributed with
4+
~ this work for additional information regarding copyright ownership.
5+
~ The ASF licenses this file to You under the Apache License, Version 2.0
6+
~ (the "License"); you may not use this file except in compliance with
7+
~ the License. You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<h2 mat-dialog-title>
19+
Flow Version Diff - {{ flowName }}
20+
</h2>
21+
<div class="flow-diff">
22+
<mat-dialog-content>
23+
<div class="dialog-content flex flex-col gap-y-4">
24+
<div
25+
class="flow-diff-message"
26+
data-qa="flow-diff-message"
27+
*ngIf="comparisonSummary.length > 0">
28+
<div class="flow-diff-message-title">Comparing versions:</div>
29+
<ul class="flow-diff-message-list">
30+
<li *ngFor="let summary of comparisonSummary">
31+
<span class="flow-diff-version">Version {{ summary.version }}</span>
32+
<span *ngIf="summary.created" class="flow-diff-created">
33+
({{ summary.created }})
34+
</span>
35+
</li>
36+
</ul>
37+
</div>
38+
39+
<div class="flow-diff-controls flex flex-wrap gap-4">
40+
<mat-form-field class="flow-diff-select" appearance="outline">
41+
<mat-label>Current Version</mat-label>
42+
<mat-select [formControl]="currentVersionControl" data-qa="current-version-select">
43+
<mat-option *ngFor="let version of versionOptions" [value]="version">
44+
{{ formatVersionOption(version) }}
45+
</mat-option>
46+
</mat-select>
47+
</mat-form-field>
48+
49+
<mat-form-field class="flow-diff-select" appearance="outline">
50+
<mat-label>Selected Version</mat-label>
51+
<mat-select [formControl]="selectedVersionControl" data-qa="selected-version-select">
52+
<mat-option *ngFor="let version of versionOptions" [value]="version">
53+
{{ formatVersionOption(version) }}
54+
</mat-option>
55+
</mat-select>
56+
</mat-form-field>
57+
58+
<mat-form-field class="flow-diff-filter" appearance="outline">
59+
<mat-label>Filter</mat-label>
60+
<input
61+
matInput
62+
type="text"
63+
[formControl]="filterControl"
64+
placeholder="Filter differences"
65+
data-qa="flow-diff-filter" />
66+
</mat-form-field>
67+
</div>
68+
69+
<div class="flow-diff-table flex-1">
70+
<div class="flow-diff-loading" *ngIf="isLoading">
71+
<mat-progress-spinner diameter="36" mode="indeterminate"></mat-progress-spinner>
72+
</div>
73+
74+
<div *ngIf="!isLoading">
75+
<div *ngIf="errorMessage" class="flow-diff-error" data-qa="flow-diff-error">
76+
{{ errorMessage }}
77+
</div>
78+
79+
<ng-container *ngIf="!errorMessage">
80+
<div *ngIf="noDifferences" class="flow-diff-empty" data-qa="flow-diff-empty">
81+
No differences to display.
82+
</div>
83+
84+
<table
85+
mat-table
86+
*ngIf="dataSource.data.length > 0"
87+
[dataSource]="dataSource"
88+
matSort
89+
matSortDisableClear
90+
data-qa="flow-diff-table">
91+
<ng-container matColumnDef="componentName">
92+
<th mat-header-cell *matHeaderCellDef mat-sort-header>Component Name</th>
93+
<td mat-cell *matCellDef="let row">
94+
<span [title]="row.componentName">{{ row.componentName || 'Unknown' }}</span>
95+
</td>
96+
</ng-container>
97+
98+
<ng-container matColumnDef="changeType">
99+
<th mat-header-cell *matHeaderCellDef mat-sort-header>Change Type</th>
100+
<td mat-cell *matCellDef="let row">
101+
<span [title]="row.changeType">{{ row.changeType }}</span>
102+
</td>
103+
</ng-container>
104+
105+
<ng-container matColumnDef="difference">
106+
<th mat-header-cell *matHeaderCellDef mat-sort-header>Difference</th>
107+
<td mat-cell *matCellDef="let row">
108+
<span [title]="row.difference">{{ row.difference }}</span>
109+
</td>
110+
</ng-container>
111+
112+
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
113+
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
114+
</table>
115+
</ng-container>
116+
</div>
117+
</div>
118+
</div>
119+
</mat-dialog-content>
120+
<mat-dialog-actions align="end">
121+
<button mat-flat-button mat-dialog-close data-qa="close-flow-diff-dialog">Close</button>
122+
</mat-dialog-actions>
123+
</div>

0 commit comments

Comments
 (0)