Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -123,6 +123,24 @@ <h2 mat-dialog-title>
</td>
</ng-container>

<!-- Actions Column -->
<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>

<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr
mat-row
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,19 @@
*/

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';
import { ErrorContextKey } from '../../../../../../../state/error';
import * as ErrorActions from '../../../../../../../state/error/error.actions';

@Component({
selector: 'change-version-dialog',
Expand All @@ -37,7 +40,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 +51,18 @@ export class ChangeVersionDialog extends CloseOnEscapeDialog {
private nifiCommon = inject(NiFiCommon);
private store = inject<Store<CanvasState>>(Store);

displayedColumns: string[] = ['current', 'version', 'created', 'comments'];
displayedColumns: string[] = ['current', 'version', 'created', 'comments', 'actions'];
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 +72,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 +158,41 @@ 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 errorContext = ErrorContextKey.FLOW_VERSION;

const dialogData: FlowDiffDialogData = {
versionControlInformation: this.versionControlInformation,
versions: this.allFlowVersions,
currentVersion: this.versionControlInformation.version,
selectedVersion: flowVersion.version,
errorContext,
clearBannerErrors: () => this.store.dispatch(ErrorActions.clearBannerErrors({ context: errorContext })),
addBannerError: (errors: string[]) =>
this.store.dispatch(
ErrorActions.addBannerError({
errorContext: {
context: errorContext,
errors
}
})
),
formatTimestamp: (metadata) => this.formatTimestamp(metadata)
};

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,128 @@
<!--
~ 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">
<context-error-banner [context]="errorContext"></context-error-banner>
<mat-dialog-content>
<div class="dialog-content flex flex-col gap-y-4">
<div
class="space-y-3 text-sm"
data-qa="flow-diff-message"
*ngIf="comparisonSummary.length > 0">
<div class="font-semibold">Comparing versions</div>
<div class="grid gap-y-3">
<div *ngFor="let summary of comparisonSummary" class="space-y-1" data-qa="flow-diff-summary-item">
<div>{{ summary.label }}</div>
<div class="tertiary-color font-medium flex flex-wrap gap-x-1">
<span>{{ summary.version }}</span>
<span *ngIf="summary.created" class="opacity-80">
({{ summary.created }})
</span>
</div>
</div>
</div>
</div>

<div class="flow-diff-controls flex flex-wrap gap-4">
<div class="w-52">
<mat-form-field class="w-full" 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>
</div>

<div class="w-52">
<mat-form-field class="w-full" 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>
</div>

<div class="flex-1 basis-60">
<mat-form-field class="w-full" 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>

<div class="listing-table flex-1 relative">
<div class="absolute inset-0 overflow-y-auto overflow-x-hidden">
<ng-container *ngIf="!isLoading && !hasError">
<div *ngIf="noDifferences" class="py-3 tertiary-color" 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
[matSortActive]="sort.active"
[matSortDirection]="sort.direction"
(matSortChange)="sortData($event)"
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