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

rfellows 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 91c0c920c75 NIFI-16406: Avoid sanitizing connection positions (#11734)
91c0c920c75 is described below

commit 91c0c920c75d8254b319b07cb6f075671f19bee1
Author: Matt Gilman <[email protected]>
AuthorDate: Thu Oct 1 12:54:52 2026 -0400

    NIFI-16406: Avoid sanitizing connection positions (#11734)
    
    * NIFI-16406: Avoid sanitizing connection positions
    
    Skip nonexistent position sanitization for connections while preserving 
bend coordinate validation in the flow designer and connector canvas.
    
    * NIFI-16406: Addressing review feedback.
---
 .../connector-canvas.effects.spec.ts               | 51 +++++++++++++++++++---
 .../connector-canvas/connector-canvas.effects.ts   | 23 +++++-----
 .../flow-designer/state/flow/flow.effects.spec.ts  | 51 ++++++++++++++++++----
 .../pages/flow-designer/state/flow/flow.effects.ts | 25 +++++------
 4 files changed, 110 insertions(+), 40 deletions(-)

diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.spec.ts
index f02aea44407..7bb534cacc6 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.spec.ts
@@ -1552,6 +1552,22 @@ describe('ConnectorCanvasEffects', () => {
             };
         }
 
+        function makeConnection(
+            id: string,
+            bendX: number,
+            bendY: number,
+            componentBendX = bendX,
+            componentBendY = bendY
+        ): any {
+            return {
+                id,
+                permissions: { canRead: true, canWrite: true },
+                revision: { version: 0 },
+                bends: [{ x: bendX, y: bendY }],
+                component: { bends: [{ x: componentBendX, y: componentBendY }] 
}
+            };
+        }
+
         function makeFlowResponse(overrides: { processors?: any[]; 
connections?: any[] } = {}): any {
             return {
                 processGroupFlow: {
@@ -1600,14 +1616,29 @@ describe('ConnectorCanvasEffects', () => {
             warnSpy.mockRestore();
         });
 
-        it('clamps catastrophic-finite connection bends to (0, 0)', async () 
=> {
+        it('does not sanitize a nonexistent connection position', async () => {
+            const { effects, actions$, mockConnectorService } = await setup();
+            const connection = makeConnection('conn-valid', 100, 200, 300, 
400);
+            (mockConnectorService.getConnectorFlow as Mock).mockReturnValue(
+                of(makeFlowResponse({ connections: [connection] }))
+            );
+
+            const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => 
undefined);
+            actions$(of(loadConnectorFlow({ connectorId: 'c-1', 
processGroupId: 'pg-1' })));
+
+            const action: any = await 
firstValueFrom(effects.loadConnectorFlow$);
+            expect(action.connections[0]).not.toHaveProperty('position');
+            expect(action.connections[0].bends[0]).toEqual({ x: 100, y: 200 });
+            expect(action.connections[0].component.bends[0]).toEqual({ x: 300, 
y: 400 });
+            expect(warnSpy).not.toHaveBeenCalled();
+            warnSpy.mockRestore();
+        });
+
+        it('clamps top-level and nested catastrophic-finite connection bends 
to (0, 0)', async () => {
             const { effects, actions$, mockConnectorService } = await setup();
             const connection = {
-                id: 'conn-bad',
-                permissions: { canRead: true, canWrite: true },
-                revision: { version: 0 },
-                position: { x: 0, y: 0 },
-                component: { bends: [{ x: 9e307, y: 0 }] }
+                ...makeConnection('conn-bad', 9e307, 0, 0, 8e307),
+                position: { x: 7e307, y: 0 }
             };
             (mockConnectorService.getConnectorFlow as Mock).mockReturnValue(
                 of(makeFlowResponse({ connections: [connection] }))
@@ -1617,7 +1648,15 @@ describe('ConnectorCanvasEffects', () => {
             actions$(of(loadConnectorFlow({ connectorId: 'c-1', 
processGroupId: 'pg-1' })));
 
             const action: any = await 
firstValueFrom(effects.loadConnectorFlow$);
+            expect(action.connections[0].bends[0]).toEqual({ x: 0, y: 0 });
             expect(action.connections[0].component.bends[0]).toEqual({ x: 0, 
y: 0 });
+            expect(action.connections[0].position).toEqual({ x: 7e307, y: 0 });
+            expect(warnSpy).toHaveBeenCalledTimes(1);
+            expect(warnSpy).toHaveBeenCalledWith(
+                expect.stringContaining('Component Connection bend 
conn-bad:bend:0 has an out-of-range position'),
+                { x: 9e307, y: 0 },
+                expect.stringContaining('falling back to (0, 0)')
+            );
             warnSpy.mockRestore();
         });
     });
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.ts
index f75e4481e4d..a6d68d55b21 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.ts
@@ -40,6 +40,7 @@ import {
     LARGE_DIALOG,
     MEDIUM_DIALOG,
     NiFiCommon,
+    Position,
     sanitizePosition,
     XL_DIALOG
 } from '@nifi/shared';
@@ -829,23 +830,21 @@ export class ConnectorCanvasEffects {
                 warnedIds: this.warnedPositionIds
             })
         });
+        const sanitizeBends = (entityId: string, bends: Position[] | 
undefined): Position[] | undefined =>
+            bends?.map((bend: Position, index: number) =>
+                sanitizePosition(bend, {
+                    componentId: `${entityId}:bend:${index}`,
+                    componentKind: 'Connection bend',
+                    warnedIds: this.warnedPositionIds
+                })
+            );
         const sanitizeConnection = (entity: any) => ({
             ...entity,
-            position: sanitizePosition(entity.position, {
-                componentId: entity.id,
-                componentKind: 'Connection',
-                warnedIds: this.warnedPositionIds
-            }),
+            bends: sanitizeBends(entity.id, entity.bends),
             component: entity.component
                 ? {
                       ...entity.component,
-                      bends: entity.component.bends?.map((bend: any, index: 
number) =>
-                          sanitizePosition(bend, {
-                              componentId: `${entity.id}:bend:${index}`,
-                              componentKind: 'Connection bend',
-                              warnedIds: this.warnedPositionIds
-                          })
-                      )
+                      bends: sanitizeBends(entity.id, entity.component.bends)
                   }
                 : entity.component
         });
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts
index 4dc36a34dd7..fbbe44253a9 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts
@@ -1439,13 +1439,19 @@ describe('FlowEffects', () => {
             };
         }
 
-        function makeConnection(id: string, bendX: number, bendY: number): any 
{
+        function makeConnection(
+            id: string,
+            bendX: number,
+            bendY: number,
+            componentBendX = bendX,
+            componentBendY = bendY
+        ): any {
             return {
                 id,
                 permissions: { canRead: true, canWrite: true },
                 revision: { version: 0 },
-                position: { x: 0, y: 0 },
-                component: { bends: [{ x: bendX, y: bendY }] }
+                bends: [{ x: bendX, y: bendY }],
+                component: { bends: [{ x: componentBendX, y: componentBendY }] 
}
             };
         }
 
@@ -1456,14 +1462,14 @@ describe('FlowEffects', () => {
                     parentGroupId: null,
                     breadcrumb: {},
                     flow: {
-                        processors: overrides.processors ?? [],
+                        processors: overrides['processors'] ?? [],
                         processGroups: [],
                         remoteProcessGroups: [],
                         inputPorts: [],
                         outputPorts: [],
                         labels: [],
                         funnels: [],
-                        connections: overrides.connections ?? []
+                        connections: overrides['connections'] ?? []
                     }
                 }
             };
@@ -1504,16 +1510,43 @@ describe('FlowEffects', () => {
             warnSpy.mockRestore();
         });
 
-        it('clamps catastrophic-finite connection bends to (0, 0)', async () 
=> {
-            const connection = makeConnection('conn-1', 9e307, 0);
+        it('does not sanitize a nonexistent connection position', async () => {
+            const connection = makeConnection('conn-valid', 100, 200, 300, 
400);
             (flowService as any).getFlow = vi.fn(() => of(buildFlowResponse({ 
connections: [connection] })));
 
             const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => 
undefined);
             action$.next(FlowActions.loadProcessGroup({ request: { id: 'pg-1', 
transitionRequired: false } }));
 
             const result: any = await 
firstValueFrom(effects.loadProcessGroup$.pipe(take(1)));
-            const bend = 
result.response.flow.processGroupFlow.flow.connections[0].component.bends[0];
-            expect(bend).toEqual({ x: 0, y: 0 });
+            const sanitizedConnection = 
result.response.flow.processGroupFlow.flow.connections[0];
+            expect(sanitizedConnection).not.toHaveProperty('position');
+            expect(sanitizedConnection.bends[0]).toEqual({ x: 100, y: 200 });
+            expect(sanitizedConnection.component.bends[0]).toEqual({ x: 300, 
y: 400 });
+            expect(warnSpy).not.toHaveBeenCalled();
+            warnSpy.mockRestore();
+        });
+
+        it('clamps top-level and nested catastrophic-finite connection bends 
to (0, 0)', async () => {
+            const connection = {
+                ...makeConnection('conn-1', 9e307, 0, 0, 8e307),
+                position: { x: 7e307, y: 0 }
+            };
+            (flowService as any).getFlow = vi.fn(() => of(buildFlowResponse({ 
connections: [connection] })));
+
+            const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => 
undefined);
+            action$.next(FlowActions.loadProcessGroup({ request: { id: 'pg-1', 
transitionRequired: false } }));
+
+            const result: any = await 
firstValueFrom(effects.loadProcessGroup$.pipe(take(1)));
+            const sanitizedConnection = 
result.response.flow.processGroupFlow.flow.connections[0];
+            expect(sanitizedConnection.bends[0]).toEqual({ x: 0, y: 0 });
+            expect(sanitizedConnection.component.bends[0]).toEqual({ x: 0, y: 
0 });
+            expect(sanitizedConnection.position).toEqual({ x: 7e307, y: 0 });
+            expect(warnSpy).toHaveBeenCalledTimes(1);
+            expect(warnSpy).toHaveBeenCalledWith(
+                expect.stringContaining('Component Connection bend 
conn-1:bend:0 has an out-of-range position'),
+                { x: 9e307, y: 0 },
+                expect.stringContaining('falling back to (0, 0)')
+            );
             warnSpy.mockRestore();
         });
 
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
index cd1fa9dd574..3858f03b346 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
@@ -5075,6 +5075,7 @@ export class FlowEffects {
      */
     private sanitizeFlowPositions(flow: ProcessGroupFlowEntity): 
ProcessGroupFlowEntity {
         const f = flow.processGroupFlow.flow;
+        type ConnectionWithBends = ConnectionEntity & { bends?: Position[] };
         const sanitize = (entity: ComponentEntity, kind: string): 
ComponentEntity => ({
             ...entity,
             position: sanitizePosition(entity.position, {
@@ -5083,23 +5084,21 @@ export class FlowEffects {
                 warnedIds: this.warnedPositionIds
             })
         });
-        const sanitizeConnection = (entity: ConnectionEntity): 
ConnectionEntity => ({
+        const sanitizeBends = (entityId: string, bends: Position[] | 
undefined): Position[] | undefined =>
+            bends?.map((bend: Position, index: number) =>
+                sanitizePosition(bend, {
+                    componentId: `${entityId}:bend:${index}`,
+                    componentKind: 'Connection bend',
+                    warnedIds: this.warnedPositionIds
+                })
+            );
+        const sanitizeConnection = (entity: ConnectionWithBends): 
ConnectionWithBends => ({
             ...entity,
-            position: sanitizePosition(entity.position, {
-                componentId: entity.id,
-                componentKind: 'Connection',
-                warnedIds: this.warnedPositionIds
-            }),
+            bends: sanitizeBends(entity.id, entity.bends),
             component: entity.component
                 ? {
                       ...entity.component,
-                      bends: entity.component.bends?.map((bend: Position, 
index: number) =>
-                          sanitizePosition(bend, {
-                              componentId: `${entity.id}:bend:${index}`,
-                              componentKind: 'Connection bend',
-                              warnedIds: this.warnedPositionIds
-                          })
-                      )
+                      bends: sanitizeBends(entity.id, entity.component.bends)
                   }
                 : entity.component
         });

Reply via email to