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 96c4fcb75e NIFI-14961: Rendering the retried relationships in the 
Connection label. (#10299)
96c4fcb75e is described below

commit 96c4fcb75e2983eafc004b1fa6d77b2121d6c851
Author: Matt Gilman <[email protected]>
AuthorDate: Tue Sep 16 11:39:45 2025 -0400

    NIFI-14961: Rendering the retried relationships in the Connection label. 
(#10299)
    
    This closes #10299
---
 .../org/apache/nifi/web/api/dto/ConnectionDTO.java |  15 ++
 .../cluster/manager/ConnectionEntityMerger.java    |   4 +
 .../org/apache/nifi/web/api/dto/DtoFactory.java    |   9 +
 .../apache/nifi/web/api/dto/DtoFactoryTest.java    | 263 +++++++++++++++++++++
 .../manager/connection-manager.service.spec.ts     |  53 +++++
 .../service/manager/connection-manager.service.ts  | 101 ++++++--
 .../flow-designer/ui/canvas/canvas.component.scss  |   9 +-
 7 files changed, 423 insertions(+), 31 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ConnectionDTO.java
 
b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ConnectionDTO.java
index 5fa1ab5e1f..47b5dc229f 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ConnectionDTO.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ConnectionDTO.java
@@ -38,6 +38,7 @@ public class ConnectionDTO extends ComponentDTO {
     private Long zIndex;
     private Set<String> selectedRelationships;
     private Set<String> availableRelationships;
+    private Set<String> retriedRelationships;
 
     private Long backPressureObjectThreshold;
     private String backPressureDataSizeThreshold;
@@ -161,6 +162,20 @@ public class ConnectionDTO extends ComponentDTO {
         this.availableRelationships = availableRelationships;
     }
 
+    /**
+     * @return relationships that are configured to be retried from the source 
of the connection. This property is read only
+     */
+    @Schema(description = "The relationships from the source of the connection 
that are configured to be retried.",
+            accessMode = Schema.AccessMode.READ_ONLY
+    )
+    public Set<String> getRetriedRelationships() {
+        return retriedRelationships;
+    }
+
+    public void setRetriedRelationships(Set<String> retriedRelationships) {
+        this.retriedRelationships = retriedRelationships;
+    }
+
     /**
      * The object count threshold for determining when back pressure is 
applied. Updating this value is a passive change in the sense that it won't 
impact whether existing files over the limit are
      * affected but it does help feeder processors to stop pushing too much 
into this work queue.
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ConnectionEntityMerger.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ConnectionEntityMerger.java
index 9885b95ac6..ef6b5afe89 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ConnectionEntityMerger.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ConnectionEntityMerger.java
@@ -61,6 +61,10 @@ public class ConnectionEntityMerger implements 
ComponentEntityMerger<ConnectionE
         if (selectedRelationships != null) {
             clientEntity.getComponent().setSelectedRelationships(new 
TreeSet<>(selectedRelationships));
         }
+        final Set<String> retriedRelationships = clientEntity.getComponent() 
== null ? null : clientEntity.getComponent().getRetriedRelationships();
+        if (retriedRelationships != null) {
+            clientEntity.getComponent().setRetriedRelationships(new 
TreeSet<>(retriedRelationships));
+        }
 
     }
 
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 51a87885e8..9a7d7b5a82 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
@@ -752,6 +752,14 @@ public final class DtoFactory {
                }
 
                
dto.getSelectedRelationships().add(selectedRelationship.getName());
+
+               // Check if this selected relationship is configured to be 
retried from the source
+               if 
(connection.getSource().isRelationshipRetried(selectedRelationship)) {
+                   if (dto.getRetriedRelationships() == null) {
+                       dto.setRetriedRelationships(new 
TreeSet<>(Collator.getInstance(Locale.US)));
+                   }
+                   
dto.getRetriedRelationships().add(selectedRelationship.getName());
+               }
            }
        }
 
@@ -4502,6 +4510,7 @@ public final class DtoFactory {
        copy.setName(original.getName());
        copy.setParentGroupId(original.getParentGroupId());
        
copy.setSelectedRelationships(copy(original.getSelectedRelationships()));
+       copy.setRetriedRelationships(copy(original.getRetriedRelationships()));
        copy.setFlowFileExpiration(original.getFlowFileExpiration());
        
copy.setBackPressureObjectThreshold(original.getBackPressureObjectThreshold());
        
copy.setBackPressureDataSizeThreshold(original.getBackPressureDataSizeThreshold());
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 cb307412ef..1d1c00bc96 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
@@ -22,10 +22,17 @@ import org.apache.nifi.bundle.BundleDetails;
 import org.apache.nifi.components.AllowableValue;
 import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.components.validation.ValidationStatus;
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.connectable.ConnectableType;
+import org.apache.nifi.connectable.Connection;
 import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.controller.queue.FlowFileQueue;
+import org.apache.nifi.controller.queue.LoadBalanceCompression;
+import org.apache.nifi.controller.queue.LoadBalanceStrategy;
 import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceProvider;
 import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.logging.LogLevel;
 import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.NarManifest;
@@ -34,6 +41,7 @@ import org.apache.nifi.nar.NarSource;
 import org.apache.nifi.nar.NarState;
 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.web.api.entity.AllowableValueEntity;
 import org.junit.jupiter.api.Test;
@@ -51,6 +59,7 @@ import java.util.stream.Collectors;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -396,4 +405,258 @@ public class DtoFactoryTest {
         final FlowRegistryClientDTO dto = 
dtoFactory.createRegistryDto(clientNode);
         assertTrue(dto.getMultipleVersionsAvailable(), "Ghost registry client 
with one compatible bundle should allow change version");
     }
+
+    @Test
+    void testCreateConnectionDtoWithRetriedRelationships() {
+        // Set up test relationships
+        final Relationship successRelationship = new 
Relationship.Builder().name("success").build();
+        final Relationship failureRelationship = new 
Relationship.Builder().name("failure").build();
+        final Relationship retryRelationship = new 
Relationship.Builder().name("retry").build();
+
+        // Mock the process group
+        final ProcessGroup processGroup = mock(ProcessGroup.class);
+        when(processGroup.getIdentifier()).thenReturn("group-id");
+
+        // Mock the source connectable
+        final Connectable sourceConnectable = mock(Connectable.class);
+        
when(sourceConnectable.getConnectableType()).thenReturn(ConnectableType.PROCESSOR);
+        when(sourceConnectable.getIdentifier()).thenReturn("source-id");
+        when(sourceConnectable.getName()).thenReturn("Source Processor");
+        
when(sourceConnectable.getProcessGroupIdentifier()).thenReturn("group-id");
+        when(sourceConnectable.getProcessGroup()).thenReturn(processGroup);
+        // Configure retry settings: only "failure" and "retry" relationships 
are retried
+        
when(sourceConnectable.isRelationshipRetried(successRelationship)).thenReturn(false);
+        
when(sourceConnectable.isRelationshipRetried(failureRelationship)).thenReturn(true);
+        
when(sourceConnectable.isRelationshipRetried(retryRelationship)).thenReturn(true);
+
+        // Mock the destination connectable
+        final Connectable destinationConnectable = mock(Connectable.class);
+        
when(destinationConnectable.getConnectableType()).thenReturn(ConnectableType.PROCESSOR);
+        when(destinationConnectable.getIdentifier()).thenReturn("dest-id");
+        when(destinationConnectable.getName()).thenReturn("Destination 
Processor");
+        
when(destinationConnectable.getProcessGroupIdentifier()).thenReturn("group-id");
+        
when(destinationConnectable.getProcessGroup()).thenReturn(processGroup);
+
+        // Mock the flow file queue
+        final FlowFileQueue flowFileQueue = mock(FlowFileQueue.class);
+        
when(flowFileQueue.getBackPressureObjectThreshold()).thenReturn(10000L);
+        when(flowFileQueue.getBackPressureDataSizeThreshold()).thenReturn("1 
GB");
+        when(flowFileQueue.getFlowFileExpiration()).thenReturn("0 sec");
+        
when(flowFileQueue.getPriorities()).thenReturn(Collections.emptyList());
+        
when(flowFileQueue.getLoadBalanceStrategy()).thenReturn(LoadBalanceStrategy.DO_NOT_LOAD_BALANCE);
+        when(flowFileQueue.getPartitioningAttribute()).thenReturn(null);
+        
when(flowFileQueue.getLoadBalanceCompression()).thenReturn(LoadBalanceCompression.DO_NOT_COMPRESS);
+        when(flowFileQueue.isActivelyLoadBalancing()).thenReturn(false);
+
+        // Mock the connection - only "failure" and "retry" are selected 
(success is not selected)
+        final Connection connection = mock(Connection.class);
+        when(connection.getIdentifier()).thenReturn("connection-id");
+        when(connection.getName()).thenReturn("Test Connection");
+        when(connection.getSource()).thenReturn(sourceConnectable);
+        when(connection.getDestination()).thenReturn(destinationConnectable);
+        when(connection.getProcessGroup()).thenReturn(processGroup);
+        when(connection.getFlowFileQueue()).thenReturn(flowFileQueue);
+        
when(connection.getRelationships()).thenReturn(Arrays.asList(failureRelationship,
 retryRelationship));
+        when(connection.getBendPoints()).thenReturn(Collections.emptyList());
+        when(connection.getLabelIndex()).thenReturn(0);
+        when(connection.getZIndex()).thenReturn(0L);
+        
when(connection.getVersionedComponentId()).thenReturn(java.util.Optional.empty());
+
+        // Create the DTO factory
+        final DtoFactory dtoFactory = new DtoFactory();
+
+        // Test the creation of ConnectionDTO
+        final ConnectionDTO connectionDto = 
dtoFactory.createConnectionDto(connection);
+
+        // Assertions
+        assertEquals("connection-id", connectionDto.getId());
+        assertEquals("Test Connection", connectionDto.getName());
+
+        // Verify selected relationships
+        assertEquals(2, connectionDto.getSelectedRelationships().size());
+        
assertTrue(connectionDto.getSelectedRelationships().contains("failure"));
+        assertTrue(connectionDto.getSelectedRelationships().contains("retry"));
+
+        // Verify retried relationships - should only contain the selected 
relationships that are also retried
+        assertEquals(2, connectionDto.getRetriedRelationships().size());
+        
assertTrue(connectionDto.getRetriedRelationships().contains("failure"));
+        assertTrue(connectionDto.getRetriedRelationships().contains("retry"));
+    }
+
+    @Test
+    void testCreateConnectionDtoWithPartiallyRetriedRelationships() {
+        // Set up test relationships
+        final Relationship successRelationship = new 
Relationship.Builder().name("success").build();
+        final Relationship failureRelationship = new 
Relationship.Builder().name("failure").build();
+
+        // Mock the process group
+        final ProcessGroup processGroup = mock(ProcessGroup.class);
+        when(processGroup.getIdentifier()).thenReturn("group-id");
+
+        // Mock the source connectable
+        final Connectable sourceConnectable = mock(Connectable.class);
+        
when(sourceConnectable.getConnectableType()).thenReturn(ConnectableType.PROCESSOR);
+        when(sourceConnectable.getIdentifier()).thenReturn("source-id");
+        when(sourceConnectable.getName()).thenReturn("Source Processor");
+        
when(sourceConnectable.getProcessGroupIdentifier()).thenReturn("group-id");
+        when(sourceConnectable.getProcessGroup()).thenReturn(processGroup);
+        // Configure retry settings: only "failure" relationship is retried, 
"success" is not
+        
when(sourceConnectable.isRelationshipRetried(successRelationship)).thenReturn(false);
+        
when(sourceConnectable.isRelationshipRetried(failureRelationship)).thenReturn(true);
+
+        // Mock the destination connectable
+        final Connectable destinationConnectable = mock(Connectable.class);
+        
when(destinationConnectable.getConnectableType()).thenReturn(ConnectableType.PROCESSOR);
+        when(destinationConnectable.getIdentifier()).thenReturn("dest-id");
+        when(destinationConnectable.getName()).thenReturn("Destination 
Processor");
+        
when(destinationConnectable.getProcessGroupIdentifier()).thenReturn("group-id");
+        
when(destinationConnectable.getProcessGroup()).thenReturn(processGroup);
+
+        // Mock the flow file queue
+        final FlowFileQueue flowFileQueue = mock(FlowFileQueue.class);
+        
when(flowFileQueue.getBackPressureObjectThreshold()).thenReturn(10000L);
+        when(flowFileQueue.getBackPressureDataSizeThreshold()).thenReturn("1 
GB");
+        when(flowFileQueue.getFlowFileExpiration()).thenReturn("0 sec");
+        
when(flowFileQueue.getPriorities()).thenReturn(Collections.emptyList());
+        
when(flowFileQueue.getLoadBalanceStrategy()).thenReturn(LoadBalanceStrategy.DO_NOT_LOAD_BALANCE);
+        when(flowFileQueue.getPartitioningAttribute()).thenReturn(null);
+        
when(flowFileQueue.getLoadBalanceCompression()).thenReturn(LoadBalanceCompression.DO_NOT_COMPRESS);
+        when(flowFileQueue.isActivelyLoadBalancing()).thenReturn(false);
+
+        // Mock the connection - both relationships are selected
+        final Connection connection = mock(Connection.class);
+        when(connection.getIdentifier()).thenReturn("connection-id");
+        when(connection.getName()).thenReturn("Test Connection");
+        when(connection.getSource()).thenReturn(sourceConnectable);
+        when(connection.getDestination()).thenReturn(destinationConnectable);
+        when(connection.getProcessGroup()).thenReturn(processGroup);
+        when(connection.getFlowFileQueue()).thenReturn(flowFileQueue);
+        
when(connection.getRelationships()).thenReturn(Arrays.asList(successRelationship,
 failureRelationship));
+        when(connection.getBendPoints()).thenReturn(Collections.emptyList());
+        when(connection.getLabelIndex()).thenReturn(0);
+        when(connection.getZIndex()).thenReturn(0L);
+        
when(connection.getVersionedComponentId()).thenReturn(java.util.Optional.empty());
+
+        // Create the DTO factory
+        final DtoFactory dtoFactory = new DtoFactory();
+
+        // Test the creation of ConnectionDTO
+        final ConnectionDTO connectionDto = 
dtoFactory.createConnectionDto(connection);
+
+        // Assertions
+        assertEquals("connection-id", connectionDto.getId());
+        assertEquals("Test Connection", connectionDto.getName());
+
+        // Verify selected relationships - should contain both
+        assertEquals(2, connectionDto.getSelectedRelationships().size());
+        
assertTrue(connectionDto.getSelectedRelationships().contains("success"));
+        
assertTrue(connectionDto.getSelectedRelationships().contains("failure"));
+
+        // Verify retried relationships - should only contain "failure" since 
"success" is not retried
+        assertEquals(1, connectionDto.getRetriedRelationships().size());
+        
assertTrue(connectionDto.getRetriedRelationships().contains("failure"));
+        
assertFalse(connectionDto.getRetriedRelationships().contains("success"));
+    }
+
+    @Test
+    void testCreateConnectionDtoWithNoRetriedRelationships() {
+        // Set up test relationships
+        final Relationship successRelationship = new 
Relationship.Builder().name("success").build();
+        final Relationship failureRelationship = new 
Relationship.Builder().name("failure").build();
+
+        // Mock the process group
+        final ProcessGroup processGroup = mock(ProcessGroup.class);
+        when(processGroup.getIdentifier()).thenReturn("group-id");
+
+        // Mock the source connectable
+        final Connectable sourceConnectable = mock(Connectable.class);
+        
when(sourceConnectable.getConnectableType()).thenReturn(ConnectableType.PROCESSOR);
+        when(sourceConnectable.getIdentifier()).thenReturn("source-id");
+        when(sourceConnectable.getName()).thenReturn("Source Processor");
+        
when(sourceConnectable.getProcessGroupIdentifier()).thenReturn("group-id");
+        when(sourceConnectable.getProcessGroup()).thenReturn(processGroup);
+        // Configure retry settings: no relationships are retried
+        
when(sourceConnectable.isRelationshipRetried(successRelationship)).thenReturn(false);
+        
when(sourceConnectable.isRelationshipRetried(failureRelationship)).thenReturn(false);
+
+        // Mock the destination connectable
+        final Connectable destinationConnectable = mock(Connectable.class);
+        
when(destinationConnectable.getConnectableType()).thenReturn(ConnectableType.PROCESSOR);
+        when(destinationConnectable.getIdentifier()).thenReturn("dest-id");
+        when(destinationConnectable.getName()).thenReturn("Destination 
Processor");
+        
when(destinationConnectable.getProcessGroupIdentifier()).thenReturn("group-id");
+        
when(destinationConnectable.getProcessGroup()).thenReturn(processGroup);
+
+        // Mock the flow file queue
+        final FlowFileQueue flowFileQueue = mock(FlowFileQueue.class);
+        
when(flowFileQueue.getBackPressureObjectThreshold()).thenReturn(10000L);
+        when(flowFileQueue.getBackPressureDataSizeThreshold()).thenReturn("1 
GB");
+        when(flowFileQueue.getFlowFileExpiration()).thenReturn("0 sec");
+        
when(flowFileQueue.getPriorities()).thenReturn(Collections.emptyList());
+        
when(flowFileQueue.getLoadBalanceStrategy()).thenReturn(LoadBalanceStrategy.DO_NOT_LOAD_BALANCE);
+        when(flowFileQueue.getPartitioningAttribute()).thenReturn(null);
+        
when(flowFileQueue.getLoadBalanceCompression()).thenReturn(LoadBalanceCompression.DO_NOT_COMPRESS);
+        when(flowFileQueue.isActivelyLoadBalancing()).thenReturn(false);
+
+        // Mock the connection
+        final Connection connection = mock(Connection.class);
+        when(connection.getIdentifier()).thenReturn("connection-id");
+        when(connection.getName()).thenReturn("Test Connection");
+        when(connection.getSource()).thenReturn(sourceConnectable);
+        when(connection.getDestination()).thenReturn(destinationConnectable);
+        when(connection.getProcessGroup()).thenReturn(processGroup);
+        when(connection.getFlowFileQueue()).thenReturn(flowFileQueue);
+        
when(connection.getRelationships()).thenReturn(Arrays.asList(successRelationship,
 failureRelationship));
+        when(connection.getBendPoints()).thenReturn(Collections.emptyList());
+        when(connection.getLabelIndex()).thenReturn(0);
+        when(connection.getZIndex()).thenReturn(0L);
+        
when(connection.getVersionedComponentId()).thenReturn(java.util.Optional.empty());
+
+        // Create the DTO factory
+        final DtoFactory dtoFactory = new DtoFactory();
+
+        // Test the creation of ConnectionDTO
+        final ConnectionDTO connectionDto = 
dtoFactory.createConnectionDto(connection);
+
+        // Assertions
+        assertEquals("connection-id", connectionDto.getId());
+        assertEquals("Test Connection", connectionDto.getName());
+
+        // Verify selected relationships
+        assertEquals(2, connectionDto.getSelectedRelationships().size());
+        
assertTrue(connectionDto.getSelectedRelationships().contains("success"));
+        
assertTrue(connectionDto.getSelectedRelationships().contains("failure"));
+
+        // Verify retried relationships - should be null since no 
relationships are retried
+        assertNull(connectionDto.getRetriedRelationships());
+    }
+
+    @Test
+    void testConnectionDtoCopy() {
+        // Create original ConnectionDTO with retried relationships
+        final ConnectionDTO original = new ConnectionDTO();
+        original.setId("connection-id");
+        original.setName("Test Connection");
+        original.setSelectedRelationships(new 
HashSet<>(Arrays.asList("success", "failure", "retry")));
+        original.setAvailableRelationships(new 
HashSet<>(Arrays.asList("success", "failure", "retry")));
+        original.setRetriedRelationships(new 
HashSet<>(Arrays.asList("failure", "retry")));
+
+        // Create the DTO factory
+        final DtoFactory dtoFactory = new DtoFactory();
+
+        // Test the copy method
+        final ConnectionDTO copy = dtoFactory.copy(original);
+
+        // Assertions
+        assertEquals(original.getId(), copy.getId());
+        assertEquals(original.getName(), copy.getName());
+        assertEquals(original.getSelectedRelationships(), 
copy.getSelectedRelationships());
+        assertEquals(original.getAvailableRelationships(), 
copy.getAvailableRelationships());
+        assertEquals(original.getRetriedRelationships(), 
copy.getRetriedRelationships());
+
+        // Verify they are separate objects (deep copy)
+        assertNotSame(original.getSelectedRelationships(), 
copy.getSelectedRelationships());
+        assertNotSame(original.getAvailableRelationships(), 
copy.getAvailableRelationships());
+        assertNotSame(original.getRetriedRelationships(), 
copy.getRetriedRelationships());
+    }
 }
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
index f4b0970bbc..fea4e539fe 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
@@ -90,4 +90,57 @@ describe('ConnectionManager', () => {
     it('should be created', () => {
         expect(service).toBeTruthy();
     });
+
+    describe('isRetryConfigured', () => {
+        it('should return true when retriedRelationships exist and have length 
> 0', () => {
+            const connection = {
+                retriedRelationships: ['failure', 'retry']
+            };
+
+            // Access private method via bracket notation for testing
+            const result = (service as any).isRetryConfigured(connection);
+
+            expect(result).toBe(true);
+        });
+
+        it('should return false when retriedRelationships is null', () => {
+            const connection = {
+                retriedRelationships: null
+            };
+
+            const result = (service as any).isRetryConfigured(connection);
+
+            expect(result).toBe(false);
+        });
+
+        it('should return false when retriedRelationships is undefined', () => 
{
+            const connection = {
+                retriedRelationships: undefined
+            };
+
+            const result = (service as any).isRetryConfigured(connection);
+
+            expect(result).toBe(false);
+        });
+
+        it('should return false when retriedRelationships is empty array', () 
=> {
+            const connection = {
+                retriedRelationships: []
+            };
+
+            const result = (service as any).isRetryConfigured(connection);
+
+            expect(result).toBe(false);
+        });
+
+        it('should return true when retriedRelationships has one 
relationship', () => {
+            const connection = {
+                retriedRelationships: ['failure']
+            };
+
+            const result = (service as any).isRetryConfigured(connection);
+
+            expect(result).toBe(true);
+        });
+    });
 });
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.ts
index 633f830b83..f82d74dc6b 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.ts
@@ -59,7 +59,7 @@ export class ConnectionManager implements OnDestroy {
     private destroyed$: Subject<boolean> = new Subject();
 
     private static readonly DIMENSIONS: Dimension = {
-        width: 224,
+        width: 240,
         height: 0
     };
 
@@ -615,6 +615,16 @@ export class ConnectionManager implements OnDestroy {
         return connection.loadBalanceStrategy != null && 'DO_NOT_LOAD_BALANCE' 
!== connection.loadBalanceStrategy;
     }
 
+    /**
+     * Determines whether retried relationships are configured for the 
specified connection.
+     *
+     * @param {object} connection
+     * @return {boolean} Whether retried relationships are configured
+     */
+    private isRetryConfigured(connection: any): boolean {
+        return connection.retriedRelationships != null && 
connection.retriedRelationships.length > 0;
+    }
+
     /**
      * Sorts the specified connections according to the z index.
      *
@@ -1221,14 +1231,6 @@ export class ConnectionManager implements OnDestroy {
 
                         
self.quickSelectBehavior.activate(connectionLabelContainer);
 
-                        // connection label
-                        connectionLabelContainer
-                            .append('rect')
-                            .attr('class', 'body')
-                            .attr('width', ConnectionManager.DIMENSIONS.width)
-                            .attr('x', 0)
-                            .attr('y', 0);
-
                         // processor border
                         connectionLabelContainer
                             .append('rect')
@@ -1236,6 +1238,14 @@ export class ConnectionManager implements OnDestroy {
                             .attr('width', ConnectionManager.DIMENSIONS.width)
                             .attr('fill', 'transparent')
                             .attr('stroke', 'transparent');
+
+                        // connection label
+                        connectionLabelContainer
+                            .append('rect')
+                            .attr('class', 'body')
+                            .attr('width', ConnectionManager.DIMENSIONS.width)
+                            .attr('x', 0)
+                            .attr('y', 0);
                     }
 
                     let labelCount = 0;
@@ -1290,12 +1300,12 @@ export class ConnectionManager implements OnDestroy {
                                     .attr('class', 'stats-value 
connection-from')
                                     .attr('x', 43)
                                     .attr('y', 14)
-                                    .attr('width', 130);
+                                    .attr('width', 146);
 
                                 connectionFrom
                                     .append('text')
                                     .attr('class', 
'connection-from-run-status')
-                                    .attr('x', 208)
+                                    .attr('x', 224)
                                     .attr('y', 14);
                             } else {
                                 
backgrounds.push(connectionFrom.select('rect.connection-label-background'));
@@ -1405,12 +1415,12 @@ export class ConnectionManager implements OnDestroy {
                                     .attr('class', 'stats-value connection-to')
                                     .attr('x', 25)
                                     .attr('y', 14)
-                                    .attr('width', 145);
+                                    .attr('width', 161);
 
                                 connectionTo
                                     .append('text')
                                     .attr('class', 'connection-to-run-status')
-                                    .attr('x', 208)
+                                    .attr('x', 224)
                                     .attr('y', 14);
                             } else {
                                 
backgrounds.push(connectionTo.select('rect.connection-label-background'));
@@ -1523,7 +1533,7 @@ export class ConnectionManager implements OnDestroy {
                                     .attr('class', 'stats-value 
connection-name')
                                     .attr('x', 45)
                                     .attr('y', 14)
-                                    .attr('width', 142);
+                                    .attr('width', 158);
                             } else {
                                 
backgrounds.push(connectionName.select('rect.connection-label-background'));
                                 
borders.push(connectionName.select('rect.connection-label-border'));
@@ -1618,11 +1628,21 @@ export class ConnectionManager implements OnDestroy {
                             })
                             .append('title');
 
+                        // retry icon
+                        queued
+                            .append('text')
+                            .attr('class', 'retry-icon')
+                            .attr('y', 14)
+                            .text(function () {
+                                return '\uf021';
+                            })
+                            .append('title');
+
                         // expiration icon
                         queued
                             .append('text')
                             .attr('class', 'expiration-icon primary-color')
-                            .attr('x', 208)
+                            .attr('x', 224)
                             .attr('y', 14)
                             .text(function () {
                                 return '\uf017';
@@ -1795,6 +1815,36 @@ export class ConnectionManager implements OnDestroy {
                         }
                     });
 
+                    // determine whether or not to show the retry icon
+                    connectionLabelContainer
+                        .select('text.retry-icon')
+                        .classed('hidden', function () {
+                            if (d.permissions.canRead) {
+                                return !self.isRetryConfigured(d.component);
+                            } else {
+                                return true;
+                            }
+                        })
+                        .classed('primary-color', function () {
+                            return d.permissions.canRead;
+                        })
+                        .attr('x', function () {
+                            let offset = 224;
+                            if (d.permissions.canRead && 
self.isExpirationConfigured(d.component)) {
+                                offset -= 16;
+                            }
+                            // retry icon comes before load-balance icon, so 
no additional offset needed here
+                            return offset;
+                        })
+                        .select('title')
+                        .text(function () {
+                            if (d.permissions.canRead && 
self.isRetryConfigured(d.component)) {
+                                return `Relationships configured to be 
retried: ${d.component.retriedRelationships.join(', ')}`;
+                            } else {
+                                return '';
+                            }
+                        });
+
                     // determine whether or not to show the load-balance icon
                     connectionLabelContainer
                         .select('text.load-balance-icon')
@@ -1811,14 +1861,16 @@ export class ConnectionManager implements OnDestroy {
                         .classed('primary-color', function (d: any) {
                             return d.permissions.canRead && 
d.component.loadBalanceStatus !== 'LOAD_BALANCE_ACTIVE';
                         })
-                        .classed('load-balance-icon-184', function () {
-                            return d.permissions.canRead && 
self.isExpirationConfigured(d.component);
-                        })
-                        .classed('load-balance-icon-200', function () {
-                            return d.permissions.canRead && 
!self.isExpirationConfigured(d.component);
-                        })
                         .attr('x', function () {
-                            return d.permissions.canRead && 
self.isExpirationConfigured(d.component) ? 192 : 208;
+                            let offset = 224;
+                            if (d.permissions.canRead && 
self.isExpirationConfigured(d.component)) {
+                                offset -= 16;
+                            }
+                            if (d.permissions.canRead && 
self.isRetryConfigured(d.component)) {
+                                offset -= 16;
+                            }
+                            // load-balance icon is leftmost, so it gets 
additional offset
+                            return offset;
                         })
                         .select('title')
                         .text(function () {
@@ -1977,7 +2029,10 @@ export class ConnectionManager implements OnDestroy {
                 if 
(!connectionLabelContainer.select('text.load-balance-icon').classed('hidden')) {
                     offset += 16;
                 }
-                return 208 - offset;
+                if 
(!connectionLabelContainer.select('text.retry-icon').classed('hidden')) {
+                    offset += 16;
+                }
+                return 224 - offset;
             })
             .select('title')
             .text(function () {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.scss
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.scss
index e4ed4efaba..5fb106d35c 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.scss
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.scss
@@ -245,19 +245,12 @@
     text.connection-to-run-status,
     text.expiration-icon,
     text.load-balance-icon,
+    text.retry-icon,
     text.penalized-icon {
         font-family: FontAwesome;
         font-size: 12px;
     }
 
-    text.load-balance-icon-184 {
-        transform-origin: 197px 10px 0;
-    }
-
-    text.load-balance-icon-200 {
-        transform-origin: 213px 10px 0;
-    }
-
     g.connection rect.backpressure-object > title > tspan,
     g.connection rect.backpressure-data-size > title > tspan {
         display: block;


Reply via email to