Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient, HttpParams } from '@angular/common/http';
import { ImportFromRegistryRequest } from '../state/flow';
import { FlowComparisonEntity, ImportFromRegistryRequest } from '../state/flow';

@Injectable({ providedIn: 'root' })
export class RegistryService {
Expand Down Expand Up @@ -63,6 +63,31 @@ export class RegistryService {
);
}

getFlowDiff(
registryId: string,
bucketId: string,
flowId: string,
versionA: string,
versionB: string,
branch?: string | null
): Observable<FlowComparisonEntity> {
const encodedRegistryId = encodeURIComponent(registryId);
const encodedBucketId = encodeURIComponent(bucketId);
const encodedFlowId = encodeURIComponent(flowId);
const encodedVersionA = encodeURIComponent(versionA);
const encodedVersionB = encodeURIComponent(versionB);

if (branch) {
const encodedBranch = encodeURIComponent(branch);
const diffEndpoint = `${RegistryService.API}/flow/registries/${encodedRegistryId}/branches/${encodedBranch}/buckets/${encodedBucketId}/flows/${encodedFlowId}/${encodedVersionA}/diff/branches/${encodedBranch}/buckets/${encodedBucketId}/flows/${encodedFlowId}/${encodedVersionB}`;
return this.httpClient.get(diffEndpoint) as Observable<FlowComparisonEntity>;
}

let params: HttpParams = new HttpParams();
const legacyEndpoint = `${RegistryService.API}/flow/registries/${encodedRegistryId}/buckets/${encodedBucketId}/flows/${encodedFlowId}/diff/${encodedVersionA}/${encodedVersionB}`;
return this.httpClient.get(legacyEndpoint, { params }) as Observable<FlowComparisonEntity>;
}

importFromRegistry(processGroupId: string, request: ImportFromRegistryRequest): Observable<any> {
return this.httpClient.post(
`${RegistryService.API}/process-groups/${processGroupId}/process-groups`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,24 @@ <h2 mat-dialog-title>
(matSortChange)="sortData($event)"
[matSortActive]="sort.active"
[matSortDirection]="sort.direction">
<!-- Actions Column -->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actions column should be displayed as the last column in the table listing.

<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
<button
mat-icon-button
type="button"
(click)="openFlowDiff(item, $event)"
aria-label="View flow version diff"
nifiTooltip
[tooltipComponentType]="TextTip"
tooltipInputData="View flow version diff"
data-qa="view-flow-diff-button">
<i class="fa fa-exchange"></i>
</button>
</td>
</ng-container>

<!-- Current Version Column -->
<ng-container matColumnDef="current">
<th mat-header-cell *matHeaderCellDef></th>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@

.listing-table {
table {
.mat-column-actions {
width: 54px;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actions column is already set for all listings so we don't need this here

Suggested change
.mat-column-actions {
width: 54px;
}

.mat-column-current {
width: 42px;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { By } from '@angular/platform-browser';
import { ChangeVersionDialog } from './change-version-dialog';
import { ChangeVersionDialogRequest } from '../../../../../state/flow';
import { VersionedFlowSnapshotMetadata } from '../../../../../../../state/shared';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/flow/flow.reducer';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
Expand All @@ -33,6 +33,7 @@ import { currentUserFeatureKey } from '../../../../../../../state/current-user';
import { canvasFeatureKey } from '../../../../../state';
import { flowFeatureKey } from '../../../../../state/flow';
import { Sort } from '@angular/material/sort';
import { FlowDiffDialog } from '../flow-diff-dialog/flow-diff-dialog';

interface SetupOptions {
dialogData?: ChangeVersionDialogRequest;
Expand Down Expand Up @@ -92,6 +93,7 @@ describe('ChangeVersionDialog', () => {
async function setup(options: SetupOptions = {}) {
const dialogData = options.dialogData || createMockDialogData();
const timeOffset = options.timeOffset || 0;
const matDialogOpen = jest.fn();

await TestBed.configureTestingModule({
imports: [ChangeVersionDialog, NoopAnimationsModule],
Expand All @@ -112,15 +114,21 @@ describe('ChangeVersionDialog', () => {
}
]
}),
{ provide: MatDialogRef, useValue: null }
{ provide: MatDialogRef, useValue: null },
{
provide: MatDialog,
useValue: {
open: matDialogOpen
}
}
]
}).compileComponents();

const fixture = TestBed.createComponent(ChangeVersionDialog);
const component = fixture.componentInstance;
fixture.detectChanges();

return { fixture, component, dialogData };
return { fixture, component, dialogData, matDialogOpen };
}

beforeEach(() => {
Expand All @@ -133,6 +141,12 @@ describe('ChangeVersionDialog', () => {
expect(component).toBeTruthy();
});

it('should include flow diff action column', async () => {
const { component } = await setup();

expect(component.displayedColumns).toContain('actions');
});

describe('Component Initialization', () => {
it('should initialize with sorted flow versions', async () => {
const { component } = await setup();
Expand Down Expand Up @@ -214,6 +228,34 @@ describe('ChangeVersionDialog', () => {
});
});

describe('Flow Diff', () => {
it('should open the flow diff dialog when the action is triggered', async () => {
const { fixture, matDialogOpen, dialogData } = await setup();
const diffButton = fixture.debugElement.query(By.css('[data-qa="view-flow-diff-button"]'));
const mockEvent = {
preventDefault: jest.fn(),
stopPropagation: jest.fn()
} as unknown as MouseEvent;

diffButton.triggerEventHandler('click', mockEvent);

expect(mockEvent.preventDefault).toHaveBeenCalled();
expect(mockEvent.stopPropagation).toHaveBeenCalled();
const icon = diffButton.query(By.css('i'));
expect(icon.nativeElement.classList).toContain('fa-exchange');
expect(matDialogOpen).toHaveBeenCalledWith(
FlowDiffDialog,
expect.objectContaining({
data: expect.objectContaining({
versionControlInformation: dialogData.versionControlInformation,
currentVersion: dialogData.versionControlInformation.version,
selectedVersion: dialogData.versions[0].versionedFlowSnapshotMetadata.version
})
})
);
});
});

describe('Selection Validation', () => {
it('should be invalid when no version is selected', async () => {
const { component } = await setup();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@
*/

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

@Component({
selector: 'change-version-dialog',
Expand All @@ -37,7 +38,8 @@ import { NiFiCommon, CloseOnEscapeDialog, NifiTooltipDirective, TextTip } from '
MatDialogModule,
MatSortModule,
MatTableModule,
NifiTooltipDirective
NifiTooltipDirective,
MatIconButton
],
templateUrl: './change-version-dialog.html',
styleUrl: './change-version-dialog.scss'
Expand All @@ -47,16 +49,18 @@ export class ChangeVersionDialog extends CloseOnEscapeDialog {
private nifiCommon = inject(NiFiCommon);
private store = inject<Store<CanvasState>>(Store);

displayedColumns: string[] = ['current', 'version', 'created', 'comments'];
displayedColumns: string[] = ['actions', 'current', 'version', 'created', 'comments'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actions column should be displayed as the last column in the table listing.

dataSource: MatTableDataSource<VersionedFlowSnapshotMetadata> =
new MatTableDataSource<VersionedFlowSnapshotMetadata>();
selectedFlowVersion: VersionedFlowSnapshotMetadata | null = null;
private allFlowVersions: VersionedFlowSnapshotMetadata[] = [];
sort: Sort = {
active: 'created',
direction: 'desc'
};
versionControlInformation: VersionControlInformation;
private timeOffset = this.store.selectSignal(selectTimeOffset);
private dialog = inject(MatDialog);

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

const flowVersions = dialogRequest.versions.map((entity) => entity.versionedFlowSnapshotMetadata);
this.allFlowVersions = flowVersions;
const sortedFlowVersions = this.sortVersions(flowVersions, this.sort);
this.selectedFlowVersion = sortedFlowVersions[0];
this.dataSource.data = sortedFlowVersions;
Expand Down Expand Up @@ -151,5 +156,27 @@ export class ChangeVersionDialog extends CloseOnEscapeDialog {
return flowVersion.version === this.versionControlInformation.version;
}

openFlowDiff(flowVersion: VersionedFlowSnapshotMetadata, event: MouseEvent) {
event.preventDefault();
event.stopPropagation();

if (!this.versionControlInformation) {
return;
}

const dialogData: FlowDiffDialogData = {
versionControlInformation: this.versionControlInformation,
versions: this.allFlowVersions,
currentVersion: this.versionControlInformation.version,
selectedVersion: flowVersion.version
};

this.dialog.open(FlowDiffDialog, {
...LARGE_DIALOG,
data: dialogData,
autoFocus: false
});
}

protected readonly TextTip = TextTip;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<h2 mat-dialog-title>
Flow Version Diff - {{ flowName }}
</h2>
<div class="flow-diff">
<mat-dialog-content>
<div class="dialog-content flex flex-col gap-y-4">
<div
class="flow-diff-message"
data-qa="flow-diff-message"
*ngIf="comparisonSummary.length > 0">
<div class="flow-diff-message-title">Comparing versions:</div>
<ul class="flow-diff-message-list">
Copy link
Contributor

@scottyaslan scottyaslan Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An unordered list is not the correct way to display this information. Please follow the label/value UX used throughout the application to display values. I believe we have an example in the change version dialog as well.

<li *ngFor="let summary of comparisonSummary">
<span class="flow-diff-version">Version {{ summary.version }}</span>
<span *ngIf="summary.created" class="flow-diff-created">
({{ summary.created }})
</span>
</li>
</ul>
</div>

<div class="flow-diff-controls flex flex-wrap gap-4">
<mat-form-field class="flow-diff-select" appearance="outline">
<mat-label>Current Version</mat-label>
<mat-select [formControl]="currentVersionControl" data-qa="current-version-select">
<mat-option *ngFor="let version of versionOptions" [value]="version">
{{ formatVersionOption(version) }}
</mat-option>
</mat-select>
</mat-form-field>

<mat-form-field class="flow-diff-select" appearance="outline">
<mat-label>Selected Version</mat-label>
<mat-select [formControl]="selectedVersionControl" data-qa="selected-version-select">
<mat-option *ngFor="let version of versionOptions" [value]="version">
{{ formatVersionOption(version) }}
</mat-option>
</mat-select>
</mat-form-field>

<mat-form-field class="flow-diff-filter" appearance="outline">
<mat-label>Filter</mat-label>
<input
matInput
type="text"
[formControl]="filterControl"
placeholder="Filter differences"
data-qa="flow-diff-filter" />
</mat-form-field>
</div>

<div class="flow-diff-table flex-1">
<div class="flow-diff-loading" *ngIf="isLoading">
<mat-progress-spinner diameter="36" mode="indeterminate"></mat-progress-spinner>
</div>
Copy link
Contributor

@scottyaslan scottyaslan Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The listings in nifi do not show mat-progress-spinner or any other loading UX.

Suggested change
<div class="flow-diff-loading" *ngIf="isLoading">
<mat-progress-spinner diameter="36" mode="indeterminate"></mat-progress-spinner>
</div>


<div *ngIf="!isLoading">
<div *ngIf="errorMessage" class="flow-diff-error" data-qa="flow-diff-error">
{{ errorMessage }}
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The listings in nifi do not show errors like this. If there is an error when requesting the diff it should be displayed in a context error banner at the top of the dialog like we do in other cases.

Suggested change
<div *ngIf="errorMessage" class="flow-diff-error" data-qa="flow-diff-error">
{{ errorMessage }}
</div>


<ng-container *ngIf="!errorMessage">
<div *ngIf="noDifferences" class="flow-diff-empty" data-qa="flow-diff-empty">
No differences to display.
</div>

<table
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ths styles for this table do not match the style for other listings throughout the app.

mat-table
*ngIf="dataSource.data.length > 0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*ngIf="dataSource.data.length > 0"

[dataSource]="dataSource"
matSort
matSortDisableClear
data-qa="flow-diff-table">
<ng-container matColumnDef="componentName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Component Name</th>
<td mat-cell *matCellDef="let row">
<span [title]="row.componentName">{{ row.componentName || 'Unknown' }}</span>
</td>
</ng-container>

<ng-container matColumnDef="changeType">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Change Type</th>
<td mat-cell *matCellDef="let row">
<span [title]="row.changeType">{{ row.changeType }}</span>
</td>
</ng-container>

<ng-container matColumnDef="difference">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Difference</th>
<td mat-cell *matCellDef="let row">
<span [title]="row.difference">{{ row.difference }}</span>
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</ng-container>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-flat-button mat-dialog-close data-qa="close-flow-diff-dialog">Close</button>
</mat-dialog-actions>
</div>
Loading