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 @@ -27,6 +27,7 @@
public class DifferenceDTO {
private String differenceType;
private String difference;
private Boolean isEnvironmental;

@Schema(description = "The type of difference")
public String getDifferenceType() {
Expand All @@ -46,6 +47,16 @@ public void setDifference(String difference) {
this.difference = difference;
}

@Schema(description = "Whether this difference is environmental (e.g., bundle version change due to NiFi upgrade) " +
"rather than a user-initiated change. Environmental changes are typically not reverted when reverting local changes.")
public Boolean getEnvironmental() {
return isEnvironmental;
}

public void setEnvironmental(Boolean environmental) {
isEnvironmental = environmental;
}

@Override
public boolean equals(final Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5614,18 +5614,20 @@ public FlowComparisonEntity getLocalModifications(final String processGroupId) {
+ " but cannot find a Flow Registry with that identifier");
}

VersionedProcessGroup registryGroup = versionControlInfo.getFlowSnapshot();
if (registryGroup == null) {
try {
final FlowVersionLocation flowVersionLocation = new FlowVersionLocation(versionControlInfo.getBranch(), versionControlInfo.getBucketIdentifier(),
versionControlInfo.getFlowIdentifier(), versionControlInfo.getVersion());
final FlowSnapshotContainer flowSnapshotContainer = flowRegistry.getFlowContents(FlowRegistryClientContextFactory.getContextForUser(NiFiUserUtils.getNiFiUser()),
flowVersionLocation, true);
final RegisteredFlowSnapshot versionedFlowSnapshot = flowSnapshotContainer.getFlowSnapshot();
registryGroup = versionedFlowSnapshot.getFlowContents();
} catch (final IOException | FlowRegistryException e) {
throw new NiFiCoreException("Failed to retrieve flow with Flow Registry in order to calculate local differences due to " + e.getMessage(), e);
}
// Always fetch the flow from the registry to get the original bundle versions.
// The cached snapshot (versionControlInfo.getFlowSnapshot()) may have been mutated
// during import/revert operations by discoverCompatibleBundles(), which would cause
// bundle version differences to not be detected.
final VersionedProcessGroup registryGroup;
try {
final FlowVersionLocation flowVersionLocation = new FlowVersionLocation(versionControlInfo.getBranch(), versionControlInfo.getBucketIdentifier(),
versionControlInfo.getFlowIdentifier(), versionControlInfo.getVersion());
final FlowSnapshotContainer flowSnapshotContainer = flowRegistry.getFlowContents(FlowRegistryClientContextFactory.getContextForUser(NiFiUserUtils.getNiFiUser()),
flowVersionLocation, true);
final RegisteredFlowSnapshot versionedFlowSnapshot = flowSnapshotContainer.getFlowSnapshot();
registryGroup = versionedFlowSnapshot.getFlowContents();
} catch (final IOException | FlowRegistryException e) {
throw new NiFiCoreException("Failed to retrieve flow with Flow Registry in order to calculate local differences due to " + e.getMessage(), e);
}

final NiFiRegistryFlowMapper mapper = makeNiFiRegistryFlowMapper(controllerFacade.getExtensionManager());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2814,7 +2814,7 @@ public Set<ComponentDifferenceDTO> createComponentDifferenceDtosForLocalModifica
if (FlowDifferenceFilters.isBundleChange(difference)) {
final ComponentDifferenceDTO componentDiff = createBundleDifference(difference);
final Set<DifferenceDTO> differences = bundleDifferencesByComponent.computeIfAbsent(componentDiff, key -> new HashSet<>());
differences.add(createDifferenceDto(difference));
differences.add(createBundleDifferenceDto(difference));
}

// Ignore any environment-specific change
Expand Down Expand Up @@ -2862,6 +2862,37 @@ DifferenceDTO createDifferenceDto(final FlowDifference difference) {
return dto;
}

/**
* Creates a DifferenceDTO for bundle changes, determining whether the change is environmental
* (due to NiFi upgrade where the original bundle version is not available) or user-initiated
* (user manually changed the bundle version when multiple versions are available).
*/
DifferenceDTO createBundleDifferenceDto(final FlowDifference difference) {
final DifferenceDTO dto = createDifferenceDto(difference);

// Determine if this bundle change is environmental (forced by upgrade) or user-initiated
// A bundle change is environmental if the original bundle version from the registry
// is not available in this NiFi instance
final Object valueA = difference.getValueA();
if (valueA instanceof org.apache.nifi.flow.Bundle registryBundle) {
final BundleCoordinate registryCoordinate = new BundleCoordinate(
registryBundle.getGroup(),
registryBundle.getArtifact(),
registryBundle.getVersion()
);

// Check if the registry bundle version is available in this NiFi instance
// If the exact bundle from the registry is not available, this is an environmental change
// caused by NiFi upgrade - the user cannot revert to a version that doesn't exist
dto.setEnvironmental(extensionManager.getBundle(registryCoordinate) == null);
} else {
// If we can't determine, assume environmental to be safe
dto.setEnvironmental(true);
}

return dto;
}

private Map<String, VersionedProcessGroup> flattenProcessGroups(final VersionedProcessGroup group) {
final Map<String, VersionedProcessGroup> flattened = new HashMap<>();
flattenProcessGroups(group, flattened);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,7 @@ export interface FlowUpdateRequestEntity {
export interface Difference {
differenceType: string;
difference: string;
environmental?: boolean;
}

export interface ComponentDifference {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ <h2 mat-dialog-title>
</div>
<div class="flex-1">
<local-changes-table
[mode]="mode"
[differences]="localModifications.componentDifferences"
(goToChange)="goToChange.next($event)"></local-changes-table>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,21 @@
<div>
<div>
<form [formGroup]="filterForm" class="my-2">
<div class="flex pt-2">
<div class="flex pt-2 items-center">
<div class="mr-2">
<mat-form-field subscriptSizing="dynamic">
<mat-label>Filter</mat-label>
<input matInput type="text" class="small" formControlName="filterTerm" />
</mat-form-field>
</div>
@if (mode === 'SHOW') {
<mat-checkbox
[checked]="showEnvironmentalChanges"
(change)="toggleEnvironmentalChanges()"
class="ml-4">
Show environmental changes ({{ environmentalCount }})
</mat-checkbox>
}
</div>
</form>
<div class="my-2 tertiary-color leading-none font-medium">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { debounceTime } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatIconButton } from '@angular/material/button';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { MatCheckbox } from '@angular/material/checkbox';

interface LocalChange {
componentType: string;
Expand All @@ -35,6 +36,7 @@ interface LocalChange {
processGroupId: string;
differenceType: string;
difference: string;
environmental?: boolean;
}

@Component({
Expand All @@ -49,7 +51,8 @@ interface LocalChange {
MatIconButton,
MatMenu,
MatMenuTrigger,
MatMenuItem
MatMenuItem,
MatCheckbox
],
templateUrl: './local-changes-table.html',
styleUrl: './local-changes-table.scss'
Expand All @@ -65,6 +68,8 @@ export class LocalChangesTable implements AfterViewInit {
filterTerm = '';
totalCount = 0;
filteredCount = 0;
environmentalCount = 0;
showEnvironmentalChanges = true;

activeSort: Sort = {
active: this.initialSortColumn,
Expand All @@ -75,8 +80,51 @@ export class LocalChangesTable implements AfterViewInit {
dataSource: MatTableDataSource<LocalChange> = new MatTableDataSource<LocalChange>();
filterForm: FormGroup;

private allLocalChanges: LocalChange[] = [];
private _mode: 'SHOW' | 'REVERT' = 'SHOW';

@Input() set mode(value: 'SHOW' | 'REVERT') {
this._mode = value;
// Re-apply filtering when mode changes (important for REVERT mode to filter environmental changes)
if (this.allLocalChanges.length > 0) {
this.updateDataSource();
}
}

get mode(): 'SHOW' | 'REVERT' {
return this._mode;
}

@Input() set differences(differences: ComponentDifference[]) {
const localChanges: LocalChange[] = this.explodeDifferences(differences);
this.allLocalChanges = this.explodeDifferences(differences);
this.environmentalCount = this.allLocalChanges.filter((change) => change.environmental === true).length;
this.updateDataSource();
}

@Output() goToChange: EventEmitter<NavigateToComponentRequest> = new EventEmitter<NavigateToComponentRequest>();

constructor() {
this.filterForm = this.formBuilder.group({ filterTerm: '', filterColumn: 'componentName' });
}

ngAfterViewInit(): void {
this.filterForm
.get('filterTerm')
?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef))
.subscribe((filterTerm: string) => {
this.applyFilter(filterTerm);
});
}

private updateDataSource(): void {
let localChanges = this.allLocalChanges;

// In REVERT mode, always filter out environmental changes as they cannot be reverted
// In SHOW mode, filter based on user preference
if (this.mode === 'REVERT' || !this.showEnvironmentalChanges) {
localChanges = localChanges.filter((change) => change.environmental !== true);
}

this.dataSource.data = this.sortEntities(localChanges, this.activeSort);
this.dataSource.filterPredicate = (data: LocalChange, filter: string) => {
const { filterTerm } = JSON.parse(filter);
Expand All @@ -96,19 +144,9 @@ export class LocalChangesTable implements AfterViewInit {
}
}

@Output() goToChange: EventEmitter<NavigateToComponentRequest> = new EventEmitter<NavigateToComponentRequest>();

constructor() {
this.filterForm = this.formBuilder.group({ filterTerm: '', filterColumn: 'componentName' });
}

ngAfterViewInit(): void {
this.filterForm
.get('filterTerm')
?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef))
.subscribe((filterTerm: string) => {
this.applyFilter(filterTerm);
});
toggleEnvironmentalChanges(): void {
this.showEnvironmentalChanges = !this.showEnvironmentalChanges;
this.updateDataSource();
}

applyFilter(filterTerm: string) {
Expand All @@ -132,6 +170,10 @@ export class LocalChangesTable implements AfterViewInit {
return item.difference;
}

isEnvironmental(item: LocalChange): boolean {
return item.environmental === true;
}

sortData(sort: Sort) {
this.activeSort = sort;
this.dataSource.data = this.sortEntities(this.dataSource.data, sort);
Expand Down Expand Up @@ -216,7 +258,8 @@ export class LocalChangesTable implements AfterViewInit {
componentType: currentValue.componentType,
processGroupId: currentValue.processGroupId,
differenceType: diff.differenceType,
difference: diff.difference
difference: diff.difference,
environmental: diff.environmental
}) as LocalChange
);
return [...accumulator, ...diffs];
Expand Down
Loading