This is an automated email from the ASF dual-hosted git repository.

mcgilman pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new f6d146f314d NIFI-15381 - Improve UX for the view Show/Revert Local 
Changes to account for environmental changes (#10681)
f6d146f314d is described below

commit f6d146f314d7885a53bba4eb0dbd3fd19fa4fb62
Author: Pierre Villard <[email protected]>
AuthorDate: Fri Feb 27 15:08:42 2026 +0100

    NIFI-15381 - Improve UX for the view Show/Revert Local Changes to account 
for environmental changes (#10681)
    
    * NIFI-15381 - Improve UX for the view Show/Revert Local Changes to account 
for environmental changes
    
    Signed-off-by: Pierre Villard <[email protected]>
    
    * review
    
    ---------
    
    Signed-off-by: Pierre Villard <[email protected]>
---
 .../org/apache/nifi/web/api/dto/DifferenceDTO.java |  15 +-
 .../apache/nifi/web/StandardNiFiServiceFacade.java |  26 ++--
 .../org/apache/nifi/web/api/dto/DtoFactory.java    |  42 ++++--
 .../apache/nifi/web/api/dto/DtoFactoryTest.java    |  59 ++++++++
 .../app/pages/flow-designer/state/flow/index.ts    |   1 +
 .../local-changes-dialog/local-changes-dialog.html |   1 +
 .../local-changes-table/local-changes-table.html   |  13 +-
 .../local-changes-table.spec.ts                    | 162 +++++++++++++++++++++
 .../local-changes-table/local-changes-table.ts     |  77 +++++++---
 9 files changed, 353 insertions(+), 43 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java
 
b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java
index 8c3d5e88d81..b144f82a184 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java
@@ -26,6 +26,7 @@ import java.util.Objects;
 public class DifferenceDTO {
     private String differenceType;
     private String difference;
+    private Boolean isEnvironmental;
 
     @Schema(description = "The type of difference")
     public String getDifferenceType() {
@@ -45,6 +46,16 @@ public class DifferenceDTO {
         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) {
@@ -54,11 +65,11 @@ public class DifferenceDTO {
             return false;
         }
         final DifferenceDTO that = (DifferenceDTO) o;
-        return Objects.equals(differenceType, that.differenceType) && 
Objects.equals(difference, that.difference);
+        return Objects.equals(differenceType, that.differenceType) && 
Objects.equals(difference, that.difference) && Objects.equals(isEnvironmental, 
that.isEnvironmental);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(differenceType, difference);
+        return Objects.hash(differenceType, difference, isEnvironmental);
     }
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index e204fde501b..55f9f6af02f 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -5615,18 +5615,20 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
                     + " 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());
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index e9c9c34851c..6954dc6607a 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -2810,7 +2810,7 @@ public final class DtoFactory {
             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
@@ -2829,17 +2829,13 @@ public final class DtoFactory {
             differences.add(createDifferenceDto(difference));
         }
 
-        if (!differencesByComponent.isEmpty()) {
-            // differences were found, so now let's add back in any 
BUNDLE_CHANGED differences
-            // since they were initially filtered out as an 
environment-specific change
-            bundleDifferencesByComponent.forEach((key, value) -> {
-                List<DifferenceDTO> values = value.stream().toList();
-                differencesByComponent.merge(key, values, (v1, v2) -> {
-                    v1.addAll(v2);
-                    return v1;
-                });
+        bundleDifferencesByComponent.forEach((key, value) -> {
+            final List<DifferenceDTO> values = value.stream().toList();
+            differencesByComponent.merge(key, values, (v1, v2) -> {
+                v1.addAll(v2);
+                return v1;
             });
-        }
+        });
 
         for (final Map.Entry<ComponentDifferenceDTO, List<DifferenceDTO>> 
entry : differencesByComponent.entrySet()) {
             entry.getKey().setDifferences(entry.getValue());
@@ -2858,6 +2854,30 @@ public final class DtoFactory {
         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);
+
+        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()
+            );
+
+            
dto.setEnvironmental(extensionManager.getBundle(registryCoordinate) == null);
+        } else {
+            dto.setEnvironmental(true);
+        }
+
+        return dto;
+    }
+
     private Map<String, VersionedProcessGroup> flattenProcessGroups(final 
VersionedProcessGroup group) {
         final Map<String, VersionedProcessGroup> flattened = new HashMap<>();
         flattenProcessGroups(group, flattened);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryTest.java
index 1d1c00bc961..e4549bdd501 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryTest.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryTest.java
@@ -43,6 +43,8 @@ import 
org.apache.nifi.nar.StandardExtensionDiscoveringManager;
 import org.apache.nifi.nar.SystemBundle;
 import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.registry.flow.FlowRegistryClientNode;
+import org.apache.nifi.registry.flow.diff.DifferenceType;
+import org.apache.nifi.registry.flow.diff.FlowDifference;
 import org.apache.nifi.web.api.entity.AllowableValueEntity;
 import org.junit.jupiter.api.Test;
 import org.slf4j.Logger;
@@ -659,4 +661,61 @@ public class DtoFactoryTest {
         assertNotSame(original.getAvailableRelationships(), 
copy.getAvailableRelationships());
         assertNotSame(original.getRetriedRelationships(), 
copy.getRetriedRelationships());
     }
+
+    @Test
+    void testCreateBundleDifferenceDtoWhenRegistryBundleAvailable() {
+        final org.apache.nifi.flow.Bundle registryBundle = new 
org.apache.nifi.flow.Bundle("com.example", "my-nar", "1.0.0");
+        final BundleCoordinate expectedCoordinate = new 
BundleCoordinate("com.example", "my-nar", "1.0.0");
+
+        final FlowDifference difference = mock(FlowDifference.class);
+        
when(difference.getDifferenceType()).thenReturn(DifferenceType.BUNDLE_CHANGED);
+        when(difference.getDescription()).thenReturn("Bundle changed from 
1.0.0 to 2.0.0");
+        when(difference.getValueA()).thenReturn(registryBundle);
+
+        final ExtensionManager extensionManager = mock(ExtensionManager.class);
+        
when(extensionManager.getBundle(eq(expectedCoordinate))).thenReturn(createBundle("com.example",
 "my-nar", "1.0.0"));
+
+        final DtoFactory dtoFactory = new DtoFactory();
+        dtoFactory.setExtensionManager(extensionManager);
+
+        final DifferenceDTO dto = 
dtoFactory.createBundleDifferenceDto(difference);
+        assertEquals(DifferenceType.BUNDLE_CHANGED.getDescription(), 
dto.getDifferenceType());
+        assertFalse(dto.getEnvironmental());
+    }
+
+    @Test
+    void testCreateBundleDifferenceDtoWhenRegistryBundleNotAvailable() {
+        final org.apache.nifi.flow.Bundle registryBundle = new 
org.apache.nifi.flow.Bundle("com.example", "my-nar", "1.0.0");
+
+        final FlowDifference difference = mock(FlowDifference.class);
+        
when(difference.getDifferenceType()).thenReturn(DifferenceType.BUNDLE_CHANGED);
+        when(difference.getDescription()).thenReturn("Bundle changed from 
1.0.0 to 2.0.0");
+        when(difference.getValueA()).thenReturn(registryBundle);
+
+        final ExtensionManager extensionManager = mock(ExtensionManager.class);
+        
when(extensionManager.getBundle(any(BundleCoordinate.class))).thenReturn(null);
+
+        final DtoFactory dtoFactory = new DtoFactory();
+        dtoFactory.setExtensionManager(extensionManager);
+
+        final DifferenceDTO dto = 
dtoFactory.createBundleDifferenceDto(difference);
+        assertEquals(DifferenceType.BUNDLE_CHANGED.getDescription(), 
dto.getDifferenceType());
+        assertTrue(dto.getEnvironmental());
+    }
+
+    @Test
+    void testCreateBundleDifferenceDtoWhenValueIsNotBundle() {
+        final FlowDifference difference = mock(FlowDifference.class);
+        
when(difference.getDifferenceType()).thenReturn(DifferenceType.BUNDLE_CHANGED);
+        when(difference.getDescription()).thenReturn("Bundle changed");
+        when(difference.getValueA()).thenReturn("not-a-bundle");
+
+        final ExtensionManager extensionManager = mock(ExtensionManager.class);
+
+        final DtoFactory dtoFactory = new DtoFactory();
+        dtoFactory.setExtensionManager(extensionManager);
+
+        final DifferenceDTO dto = 
dtoFactory.createBundleDifferenceDto(difference);
+        assertTrue(dto.getEnvironmental());
+    }
 }
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
index da79c1c19b5..832c608d620 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
@@ -865,6 +865,7 @@ export interface FlowUpdateRequestEntity {
 export interface Difference {
     differenceType: string;
     difference: string;
+    environmental?: boolean;
 }
 
 export interface ComponentDifference {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-dialog.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-dialog.html
index 93fdfae04cf..343a7a015e5 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-dialog.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-dialog.html
@@ -34,6 +34,7 @@
             </div>
             <div class="flex-1">
                 <local-changes-table
+                    [mode]="mode"
                     [differences]="localModifications.componentDifferences"
                     
(goToChange)="goToChange.next($event)"></local-changes-table>
             </div>
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-table/local-changes-table.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-table/local-changes-table.html
index 121a245abb5..8f95bc2845e 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-table/local-changes-table.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-table/local-changes-table.html
@@ -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">
@@ -55,6 +63,9 @@
                 <ng-container matColumnDef="changeType">
                     <th mat-header-cell *matHeaderCellDef 
mat-sort-header>Change Type</th>
                     <td mat-cell *matCellDef="let item" 
[title]="formatChangeType(item)">
+                        @if (isEnvironmental(item)) {
+                            <i class="fa fa-info-circle neutral-color mr-2" 
title="Environmental change"></i>
+                        }
                         {{ formatChangeType(item) }}
                     </td>
                 </ng-container>
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-table/local-changes-table.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-table/local-changes-table.spec.ts
index 105ebf9d26a..305588c3f1e 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-table/local-changes-table.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-table/local-changes-table.spec.ts
@@ -20,11 +20,42 @@ import { ComponentFixture, TestBed } from 
'@angular/core/testing';
 import { LocalChangesTable } from './local-changes-table';
 import { NoopAnimationsModule } from '@angular/platform-browser/animations';
 import { ComponentType } from '@nifi/shared';
+import { ComponentDifference } from '../../../../../../state/flow';
 
 describe('LocalChangesTable', () => {
     let component: LocalChangesTable;
     let fixture: ComponentFixture<LocalChangesTable>;
 
+    const mixedDifferences: ComponentDifference[] = [
+        {
+            componentType: ComponentType.Processor,
+            componentId: '1',
+            processGroupId: 'pg-1',
+            componentName: 'GenerateFlowFile',
+            differences: [
+                { differenceType: 'Property Value Changed', difference: 'Batch 
Size changed from 1 to 10' },
+                {
+                    differenceType: 'Component Bundle Changed',
+                    difference: 'Bundle changed from 1.0 to 2.0',
+                    environmental: true
+                }
+            ]
+        },
+        {
+            componentType: ComponentType.ControllerService,
+            componentId: '2',
+            processGroupId: 'pg-1',
+            componentName: 'DBCPService',
+            differences: [
+                {
+                    differenceType: 'Component Bundle Changed',
+                    difference: 'Bundle changed from 1.0 to 2.0',
+                    environmental: false
+                }
+            ]
+        }
+    ];
+
     beforeEach(async () => {
         await TestBed.configureTestingModule({
             imports: [LocalChangesTable, NoopAnimationsModule]
@@ -102,4 +133,135 @@ describe('LocalChangesTable', () => {
             });
         });
     });
+
+    describe('isEnvironmental', () => {
+        it('should return true when environmental is true', () => {
+            expect(
+                component.isEnvironmental({
+                    componentType: 'Processor',
+                    componentId: '1',
+                    componentName: 'P',
+                    processGroupId: 'pg',
+                    differenceType: 'Bundle Changed',
+                    difference: 'diff',
+                    environmental: true
+                })
+            ).toBe(true);
+        });
+
+        it('should return false when environmental is false', () => {
+            expect(
+                component.isEnvironmental({
+                    componentType: 'Processor',
+                    componentId: '1',
+                    componentName: 'P',
+                    processGroupId: 'pg',
+                    differenceType: 'Bundle Changed',
+                    difference: 'diff',
+                    environmental: false
+                })
+            ).toBe(false);
+        });
+
+        it('should return false when environmental is undefined', () => {
+            expect(
+                component.isEnvironmental({
+                    componentType: 'Processor',
+                    componentId: '1',
+                    componentName: 'P',
+                    processGroupId: 'pg',
+                    differenceType: 'Changed',
+                    difference: 'diff'
+                })
+            ).toBe(false);
+        });
+    });
+
+    describe('environmentalCount', () => {
+        it('should count environmental changes from input differences', () => {
+            component.differences = mixedDifferences;
+            expect(component.environmentalCount).toBe(1);
+        });
+    });
+
+    describe('SHOW mode filtering', () => {
+        beforeEach(() => {
+            component.mode = 'SHOW';
+        });
+
+        it('should hide environmental changes by default', () => {
+            component.differences = mixedDifferences;
+            expect(component.showEnvironmentalChanges).toBe(false);
+            expect(component.dataSource.data.length).toBe(2);
+            expect(component.dataSource.data.every((d) => d.environmental !== 
true)).toBe(true);
+        });
+
+        it('should include environmental changes when toggle is enabled', () 
=> {
+            component.differences = mixedDifferences;
+            component.toggleEnvironmentalChanges();
+            expect(component.showEnvironmentalChanges).toBe(true);
+            expect(component.dataSource.data.length).toBe(3);
+        });
+
+        it('should set totalCount to all changes including environmental', () 
=> {
+            component.differences = mixedDifferences;
+            expect(component.totalCount).toBe(3);
+        });
+
+        it('should set filteredCount to displayed changes only', () => {
+            component.differences = mixedDifferences;
+            expect(component.filteredCount).toBe(2);
+
+            component.toggleEnvironmentalChanges();
+            expect(component.filteredCount).toBe(3);
+        });
+    });
+
+    describe('REVERT mode filtering', () => {
+        beforeEach(() => {
+            component.mode = 'REVERT';
+        });
+
+        it('should always filter out environmental changes', () => {
+            component.differences = mixedDifferences;
+            expect(component.dataSource.data.length).toBe(2);
+            expect(component.dataSource.data.every((d) => d.environmental !== 
true)).toBe(true);
+        });
+
+        it('should not include environmental changes even after toggle', () => 
{
+            component.differences = mixedDifferences;
+            component.showEnvironmentalChanges = true;
+            component.differences = mixedDifferences;
+            expect(component.dataSource.data.length).toBe(2);
+        });
+
+        it('should set totalCount to non-environmental changes only', () => {
+            component.differences = mixedDifferences;
+            expect(component.totalCount).toBe(2);
+        });
+
+        it('should set filteredCount equal to totalCount', () => {
+            component.differences = mixedDifferences;
+            expect(component.filteredCount).toBe(2);
+            expect(component.totalCount).toBe(component.filteredCount);
+        });
+    });
+
+    describe('toggleEnvironmentalChanges', () => {
+        it('should toggle the flag and re-filter', () => {
+            component.mode = 'SHOW';
+            component.differences = mixedDifferences;
+
+            expect(component.showEnvironmentalChanges).toBe(false);
+            expect(component.dataSource.data.length).toBe(2);
+
+            component.toggleEnvironmentalChanges();
+            expect(component.showEnvironmentalChanges).toBe(true);
+            expect(component.dataSource.data.length).toBe(3);
+
+            component.toggleEnvironmentalChanges();
+            expect(component.showEnvironmentalChanges).toBe(false);
+            expect(component.dataSource.data.length).toBe(2);
+        });
+    });
 });
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-table/local-changes-table.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-table/local-changes-table.ts
index f42b94314fb..1041eb14721 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-table/local-changes-table.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-table/local-changes-table.ts
@@ -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;
@@ -35,6 +36,7 @@ interface LocalChange {
     processGroupId: string;
     differenceType: string;
     difference: string;
+    environmental?: boolean;
 }
 
 @Component({
@@ -49,7 +51,8 @@ interface LocalChange {
         MatIconButton,
         MatMenu,
         MatMenuTrigger,
-        MatMenuItem
+        MatMenuItem,
+        MatCheckbox
     ],
     templateUrl: './local-changes-table.html',
     styleUrl: './local-changes-table.scss'
@@ -65,6 +68,8 @@ export class LocalChangesTable implements AfterViewInit {
     filterTerm = '';
     totalCount = 0;
     filteredCount = 0;
+    environmentalCount = 0;
+    showEnvironmentalChanges = false;
 
     activeSort: Sort = {
         active: this.initialSortColumn,
@@ -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);
@@ -86,7 +134,7 @@ export class LocalChangesTable implements AfterViewInit {
                 this.nifiCommon.stringContains(data.differenceType, 
filterTerm, true)
             );
         };
-        this.totalCount = localChanges.length;
+        this.totalCount = this.mode === 'REVERT' ? localChanges.length : 
this.allLocalChanges.length;
         this.filteredCount = localChanges.length;
 
         // apply any filtering to the new data
@@ -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) {
@@ -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);
@@ -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];

Reply via email to