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 b653e0a78f5 NIFI-16156: Add connector-scoped Controller Service GET so 
View Canva… (#11487)
b653e0a78f5 is described below

commit b653e0a78f59ccb9c9eecbcd96c2b90e8e423022
Author: Matt Gilman <[email protected]>
AuthorDate: Mon Aug 3 09:59:52 2026 -0400

    NIFI-16156: Add connector-scoped Controller Service GET so View Canva… 
(#11487)
    
    * NIFI-16156: Add connector-scoped Controller Service GET so View Canvas Go 
To Service works without Troubleshooting.
    
    - Adding response merging for the Controller Service endpoints.
    
    * NIFI-16156: Addressing review feedback.
    
    This closes #11487
---
 .../endpoints/ControllerServiceEndpointMerger.java |  3 +
 .../ControllerServicesEndpointMerger.java          |  6 +-
 .../ControllerServiceEndpointMergerTest.java       | 59 ++++++++++++++
 .../ControllerServicesEndpointMergerTest.java      | 63 +++++++++++++++
 .../org/apache/nifi/web/NiFiServiceFacade.java     | 11 +++
 .../apache/nifi/web/StandardNiFiServiceFacade.java |  7 ++
 .../org/apache/nifi/web/api/ConnectorResource.java | 60 ++++++++++++++
 .../nifi/web/StandardNiFiServiceFacadeTest.java    | 20 +++++
 .../apache/nifi/web/api/TestConnectorResource.java | 45 +++++++++++
 .../pages/connectors/service/connector.service.ts  | 10 ++-
 .../state/connector-canvas/bind-go-to-service.ts   | 90 +++++++++++++++++++++
 .../connector-canvas.effects.spec.ts               | 87 ++++++++++++++++++++
 .../connector-canvas/connector-canvas.effects.ts   | 10 ++-
 .../connector-controller-services.effects.spec.ts  | 93 ++++++++++++++++++++--
 .../connector-controller-services.effects.ts       | 11 ++-
 .../nifi/toolkit/client/ConnectorClient.java       | 12 +++
 .../toolkit/client/impl/JerseyConnectorClient.java | 20 +++++
 17 files changed, 598 insertions(+), 9 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMerger.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMerger.java
index 3d79a83e778..4e740cc84b0 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMerger.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMerger.java
@@ -33,12 +33,15 @@ public class ControllerServiceEndpointMerger extends 
AbstractSingleEntityEndpoin
     public static final Pattern PROCESS_GROUPS_CONTROLLER_SERVICES_URI = 
Pattern.compile("/nifi-api/process-groups/(?:(?:root)|(?:[a-f0-9\\-]{36}))/controller-services");
     public static final Pattern CONTROLLER_SERVICE_URI_PATTERN = 
Pattern.compile("/nifi-api/controller-services/[a-f0-9\\-]{36}");
     public static final Pattern CONTROLLER_SERVICE_RUN_STATUS_URI_PATTERN = 
Pattern.compile("/nifi-api/controller-services/[a-f0-9\\-]{36}/run-status");
+    public static final Pattern CONNECTOR_CONTROLLER_SERVICE_URI_PATTERN = 
Pattern.compile("/nifi-api/connectors/[a-f0-9\\-]{36}/controller-services/[a-f0-9\\-]{36}");
     private final ControllerServiceEntityMerger controllerServiceEntityMerger 
= new ControllerServiceEntityMerger();
 
     @Override
     public boolean canHandle(URI uri, String method) {
         if (("GET".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) 
&& CONTROLLER_SERVICE_URI_PATTERN.matcher(uri.getPath()).matches()) {
             return true;
+        } else if ("GET".equalsIgnoreCase(method) && 
CONNECTOR_CONTROLLER_SERVICE_URI_PATTERN.matcher(uri.getPath()).matches()) {
+            return true;
         } else if ("PUT".equalsIgnoreCase(method) && 
CONTROLLER_SERVICE_RUN_STATUS_URI_PATTERN.matcher(uri.getPath()).matches()) {
             return true;
         } else if ("POST".equalsIgnoreCase(method) && 
(CONTROLLER_CONTROLLER_SERVICES_URI.equals(uri.getPath()) || 
PROCESS_GROUPS_CONTROLLER_SERVICES_URI.matcher(uri.getPath()).matches())) {
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServicesEndpointMerger.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServicesEndpointMerger.java
index 78c1aade0e1..03cd860f860 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServicesEndpointMerger.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServicesEndpointMerger.java
@@ -33,10 +33,14 @@ import java.util.regex.Pattern;
 public class ControllerServicesEndpointMerger implements 
EndpointResponseMerger {
     public static final String CONTROLLER_SERVICES_URI = 
"/nifi-api/flow/controller/controller-services";
     public static final Pattern PROCESS_GROUPS_CONTROLLER_SERVICES_URI = 
Pattern.compile("/nifi-api/flow/process-groups/(?:(?:root)|(?:[a-f0-9\\-]{36}))/controller-services");
+    public static final Pattern 
CONNECTOR_PROCESS_GROUPS_CONTROLLER_SERVICES_URI =
+            
Pattern.compile("/nifi-api/connectors/[a-f0-9\\-]{36}/flow/process-groups/[a-f0-9\\-]{36}/controller-services");
 
     @Override
     public boolean canHandle(URI uri, String method) {
-        return "GET".equalsIgnoreCase(method) && 
(CONTROLLER_SERVICES_URI.equals(uri.getPath()) || 
PROCESS_GROUPS_CONTROLLER_SERVICES_URI.matcher(uri.getPath()).matches());
+        return "GET".equalsIgnoreCase(method) && 
(CONTROLLER_SERVICES_URI.equals(uri.getPath())
+                || 
PROCESS_GROUPS_CONTROLLER_SERVICES_URI.matcher(uri.getPath()).matches()
+                || 
CONNECTOR_PROCESS_GROUPS_CONTROLLER_SERVICES_URI.matcher(uri.getPath()).matches());
     }
 
     @Override
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMergerTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMergerTest.java
new file mode 100644
index 00000000000..9f08e200252
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMergerTest.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.cluster.coordination.http.endpoints;
+
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ControllerServiceEndpointMergerTest {
+
+    private static final String CONNECTOR_ID = 
"12345678-1234-1234-1234-123456789012";
+    private static final String CONTROLLER_SERVICE_ID = 
"abcdef01-2345-6789-abcd-ef0123456789";
+
+    @Test
+    public void testCanHandleControllerServiceUri() {
+        final ControllerServiceEndpointMerger merger = new 
ControllerServiceEndpointMerger();
+        final String controllerServiceUri = "/nifi-api/controller-services/" + 
CONTROLLER_SERVICE_ID;
+
+        assertTrue(merger.canHandle(URI.create(controllerServiceUri), "GET"));
+        assertTrue(merger.canHandle(URI.create(controllerServiceUri), "PUT"));
+        assertFalse(merger.canHandle(URI.create(controllerServiceUri), 
"DELETE"));
+    }
+
+    @Test
+    public void testCanHandleConnectorControllerServiceUri() {
+        final ControllerServiceEndpointMerger merger = new 
ControllerServiceEndpointMerger();
+        final String connectorControllerServiceUri = "/nifi-api/connectors/" + 
CONNECTOR_ID + "/controller-services/" + CONTROLLER_SERVICE_ID;
+
+        // Test valid URIs
+        assertTrue(merger.canHandle(URI.create(connectorControllerServiceUri), 
"GET"));
+        assertTrue(merger.canHandle(URI.create(connectorControllerServiceUri + 
"?uiOnly=true"), "GET"));
+
+        // A connector-managed Controller Service is only ever retrieved via 
GET; PUT is not a supported operation on this path.
+        
assertFalse(merger.canHandle(URI.create(connectorControllerServiceUri), "PUT"));
+
+        // Test invalid URIs
+        assertFalse(merger.canHandle(URI.create(connectorControllerServiceUri 
+ "/state"), "GET"));
+        assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/" + 
CONNECTOR_ID + "/controller-services"), "GET"));
+        
assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/not-a-uuid/controller-services/"
 + CONTROLLER_SERVICE_ID), "GET"));
+        assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/" + 
CONNECTOR_ID + "/controller-services/not-a-uuid"), "GET"));
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServicesEndpointMergerTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServicesEndpointMergerTest.java
new file mode 100644
index 00000000000..a3122eec1ac
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServicesEndpointMergerTest.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.cluster.coordination.http.endpoints;
+
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ControllerServicesEndpointMergerTest {
+
+    private static final String CONNECTOR_ID = 
"12345678-1234-1234-1234-123456789012";
+    private static final String PROCESS_GROUP_ID = 
"abcdef01-2345-6789-abcd-ef0123456789";
+
+    @Test
+    public void testCanHandleControllerControllerServicesUri() {
+        final ControllerServicesEndpointMerger merger = new 
ControllerServicesEndpointMerger();
+
+        
assertTrue(merger.canHandle(URI.create("/nifi-api/flow/controller/controller-services"),
 "GET"));
+        
assertFalse(merger.canHandle(URI.create("/nifi-api/flow/controller/controller-services"),
 "POST"));
+    }
+
+    @Test
+    public void testCanHandleProcessGroupControllerServicesUri() {
+        final ControllerServicesEndpointMerger merger = new 
ControllerServicesEndpointMerger();
+
+        
assertTrue(merger.canHandle(URI.create("/nifi-api/flow/process-groups/root/controller-services"),
 "GET"));
+        
assertTrue(merger.canHandle(URI.create("/nifi-api/flow/process-groups/" + 
PROCESS_GROUP_ID + "/controller-services"), "GET"));
+    }
+
+    @Test
+    public void testCanHandleConnectorProcessGroupControllerServicesUri() {
+        final ControllerServicesEndpointMerger merger = new 
ControllerServicesEndpointMerger();
+        final String connectorProcessGroupControllerServicesUri =
+                "/nifi-api/connectors/" + CONNECTOR_ID + 
"/flow/process-groups/" + PROCESS_GROUP_ID + "/controller-services";
+
+        // Test valid URIs
+        
assertTrue(merger.canHandle(URI.create(connectorProcessGroupControllerServicesUri),
 "GET"));
+        
assertTrue(merger.canHandle(URI.create(connectorProcessGroupControllerServicesUri
 + "?includeDescendantGroups=true"), "GET"));
+
+        // Test invalid URIs
+        
assertFalse(merger.canHandle(URI.create(connectorProcessGroupControllerServicesUri),
 "POST"));
+        assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/" + 
CONNECTOR_ID + "/flow/process-groups/" + PROCESS_GROUP_ID), "GET"));
+        
assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/not-a-uuid/flow/process-groups/"
 + PROCESS_GROUP_ID + "/controller-services"), "GET"));
+        assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/" + 
CONNECTOR_ID + "/flow/process-groups/not-a-uuid/controller-services"), "GET"));
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index 031ca9acafd..2313b34f977 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -2320,6 +2320,17 @@ public interface NiFiServiceFacade {
      */
     ComponentStateDTO clearConnectorProcessorState(String connectorId, String 
processorId, ComponentStateDTO componentStateDTO);
 
+    /**
+     * Gets a controller service within a connector's managed process group. 
Available regardless of whether
+     * the Connector is in Troubleshooting mode.
+     *
+     * @param connectorId         the connector id
+     * @param controllerServiceId the controller service id
+     * @param includeReferencingComponents whether to include referencing 
components in the response
+     * @return the controller service entity
+     */
+    ControllerServiceEntity getConnectorControllerService(String connectorId, 
String controllerServiceId, boolean includeReferencingComponents);
+
     /**
      * Gets the state for a controller service within a connector's managed 
process group.
      *
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 8ee7f4f64f2..6688dd48152 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
@@ -2192,6 +2192,13 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
         return getConnectorProcessorState(connectorId, processorId);
     }
 
+    @Override
+    public ControllerServiceEntity getConnectorControllerService(final String 
connectorId, final String controllerServiceId,
+            final boolean includeReferencingComponents) {
+        final ControllerServiceNode controllerService = 
locateConnectorControllerService(connectorId, controllerServiceId);
+        return createControllerServiceEntity(controllerService, 
includeReferencingComponents);
+    }
+
     @Override
     public ComponentStateDTO getConnectorControllerServiceState(final String 
connectorId, final String controllerServiceId) {
         final ControllerServiceNode controllerService = 
locateConnectorControllerService(connectorId, controllerServiceId);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ConnectorResource.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ConnectorResource.java
index 2e28320c1c9..7836582db82 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ConnectorResource.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ConnectorResource.java
@@ -3094,6 +3094,66 @@ public class ConnectorResource extends 
ApplicationResource {
         );
     }
 
+    // -----------------
+    // Controller Services
+    // -----------------
+
+    /**
+     * Gets a controller service within a connector. Available regardless of 
whether the Connector is in Troubleshooting mode.
+     *
+     * @param connectorId         the connector id
+     * @param controllerServiceId the controller service id
+     * @param uiOnly              whether to strip non-UI-relevant fields from 
the response
+     * @return a ControllerServiceEntity
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}/controller-services/{controllerServiceId}")
+    @Operation(
+            summary = "Gets a controller service within a connector",
+            responses = {
+                    @ApiResponse(responseCode = "200", content = 
@Content(schema = @Schema(implementation = ControllerServiceEntity.class))),
+                    @ApiResponse(responseCode = "400", description = "NiFi was 
unable to complete the request because it was invalid. The request should not 
be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client 
could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client 
is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The 
specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The 
request was valid but NiFi was not in the appropriate state to process it.")
+            },
+            security = {
+                    @SecurityRequirement(name = "Read - /connectors/{uuid}")
+            },
+            description = "If the uiOnly query parameter is provided with a 
value of true, the returned entity may only contain " +
+                    "fields that are necessary for rendering the NiFi User 
Interface. As such, the selected fields may change " +
+                    "at any time, even during incremental releases, without 
warning. As a result, this parameter should not be " +
+                    "provided by any client other than the UI."
+    )
+    public Response getConnectorControllerService(
+            @Parameter(description = "The connector id.", required = true)
+            @PathParam("id") final String connectorId,
+            @Parameter(description = "The controller service id.", required = 
true)
+            @PathParam("controllerServiceId") final String controllerServiceId,
+            @QueryParam("uiOnly") @DefaultValue("false") final boolean uiOnly) 
{
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.GET);
+        }
+
+        serviceFacade.authorizeAccess(lookup -> {
+            final Authorizable connector = lookup.getConnector(connectorId);
+            connector.authorize(authorizer, RequestAction.READ, 
NiFiUserUtils.getNiFiUser());
+        });
+
+        // Resolve the controller service from the managed Process Group tree 
so the Troubleshooting access gate does not apply.
+        final ControllerServiceEntity entity = 
serviceFacade.getConnectorControllerService(connectorId, controllerServiceId, 
true);
+        if (uiOnly) {
+            stripNonUiRelevantFields(entity);
+        }
+        
controllerServiceResource.populateRemainingControllerServiceEntityContent(entity);
+
+        return generateOkResponse(entity).build();
+    }
+
     // -----------------
     // Controller Service State
     // -----------------
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
index 8598124c546..e73cdf17023 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
@@ -2167,6 +2167,26 @@ public class StandardNiFiServiceFacadeTest {
         verify(componentStateDAO).clearState(processorNode, null);
     }
 
+    @Test
+    public void testGetConnectorControllerServiceNotFound() {
+        final String connectorId = "connector-id";
+        final String controllerServiceId = 
"non-existent-controller-service-id";
+
+        final ConnectorDAO connectorDAO = mock(ConnectorDAO.class);
+        serviceFacade.setConnectorDAO(connectorDAO);
+
+        final ConnectorNode connectorNode = mock(ConnectorNode.class);
+        final FrameworkFlowContext flowContext = 
mock(FrameworkFlowContext.class);
+        final ProcessGroup managedProcessGroup = mock(ProcessGroup.class);
+
+        when(connectorDAO.getConnector(connectorId, 
ConnectorSyncMode.LOCAL_ONLY)).thenReturn(connectorNode);
+        when(connectorNode.getActiveFlowContext()).thenReturn(flowContext);
+        
when(flowContext.getManagedProcessGroup()).thenReturn(managedProcessGroup);
+        when(managedProcessGroup.findControllerService(controllerServiceId, 
false, true)).thenReturn(null);
+
+        assertThrows(ResourceNotFoundException.class, () -> 
serviceFacade.getConnectorControllerService(connectorId, controllerServiceId, 
false));
+    }
+
     @Test
     public void testGetConnectorControllerServiceState() {
         final String connectorId = "connector-id";
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestConnectorResource.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestConnectorResource.java
index 5f9d2373807..b09de3509be 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestConnectorResource.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestConnectorResource.java
@@ -921,6 +921,51 @@ public class TestConnectorResource {
         verify(serviceFacade, 
never()).clearConnectorProcessorState(anyString(), anyString(), any());
     }
 
+    @Test
+    public void testGetConnectorControllerService() {
+        final ControllerServiceEntity controllerServiceEntity = new 
ControllerServiceEntity();
+        controllerServiceEntity.setId(CONTROLLER_SERVICE_ID);
+        when(serviceFacade.getConnectorControllerService(CONNECTOR_ID, 
CONTROLLER_SERVICE_ID, true)).thenReturn(controllerServiceEntity);
+        
when(controllerServiceResource.populateRemainingControllerServiceEntityContent(controllerServiceEntity)).thenReturn(controllerServiceEntity);
+
+        try (Response response = 
connectorResource.getConnectorControllerService(CONNECTOR_ID, 
CONTROLLER_SERVICE_ID, false)) {
+            assertEquals(200, response.getStatus());
+            assertEquals(controllerServiceEntity, response.getEntity());
+        }
+
+        verify(serviceFacade).authorizeAccess(any(AuthorizeAccess.class));
+        verify(serviceFacade).getConnectorControllerService(CONNECTOR_ID, 
CONTROLLER_SERVICE_ID, true);
+        
verify(controllerServiceResource).populateRemainingControllerServiceEntityContent(controllerServiceEntity);
+    }
+
+    @Test
+    public void testGetConnectorControllerServiceUiOnly() {
+        final ControllerServiceEntity controllerServiceEntity = new 
ControllerServiceEntity();
+        controllerServiceEntity.setId(CONTROLLER_SERVICE_ID);
+        when(serviceFacade.getConnectorControllerService(CONNECTOR_ID, 
CONTROLLER_SERVICE_ID, true)).thenReturn(controllerServiceEntity);
+        
when(controllerServiceResource.populateRemainingControllerServiceEntityContent(controllerServiceEntity)).thenReturn(controllerServiceEntity);
+
+        try (Response response = 
connectorResource.getConnectorControllerService(CONNECTOR_ID, 
CONTROLLER_SERVICE_ID, true)) {
+            assertEquals(200, response.getStatus());
+            assertEquals(controllerServiceEntity, response.getEntity());
+        }
+
+        verify(serviceFacade).authorizeAccess(any(AuthorizeAccess.class));
+        verify(serviceFacade).getConnectorControllerService(CONNECTOR_ID, 
CONTROLLER_SERVICE_ID, true);
+        
verify(controllerServiceResource).populateRemainingControllerServiceEntityContent(controllerServiceEntity);
+    }
+
+    @Test
+    public void testGetConnectorControllerServiceNotAuthorized() {
+        
doThrow(AccessDeniedException.class).when(serviceFacade).authorizeAccess(any(AuthorizeAccess.class));
+
+        assertThrows(AccessDeniedException.class, () ->
+            connectorResource.getConnectorControllerService(CONNECTOR_ID, 
CONTROLLER_SERVICE_ID, false));
+
+        verify(serviceFacade).authorizeAccess(any(AuthorizeAccess.class));
+        verify(serviceFacade, 
never()).getConnectorControllerService(anyString(), anyString(), eq(true));
+    }
+
     @Test
     public void testGetConnectorControllerServiceState() {
         final ComponentStateDTO stateDTO = new ComponentStateDTO();
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/service/connector.service.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/service/connector.service.ts
index 991c2e13bf7..2c8dcb56744 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/service/connector.service.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/service/connector.service.ts
@@ -23,7 +23,7 @@ import { Client } from '../../../service/client.service';
 import { ClusterConnectionService } from 
'../../../service/cluster-connection.service';
 import { ConnectorsResponse, CreateConnectorRequest } from '../state';
 import { ConnectorEntity } from '@nifi/shared';
-import { ParameterContextEntity, SearchResultsEntity } from 
'../../../state/shared';
+import { ControllerServiceEntity, ParameterContextEntity, SearchResultsEntity 
} from '../../../state/shared';
 import { DropRequestEntity } from '../../../state/empty-queue';
 
 @Injectable({ providedIn: 'root' })
@@ -124,6 +124,14 @@ export class ConnectorService {
         );
     }
 
+    getControllerService(connectorId: string, controllerServiceId: string): 
Observable<ControllerServiceEntity> {
+        const params: Record<string, boolean> = { uiOnly: true };
+        return this.httpClient.get<ControllerServiceEntity>(
+            
`${ConnectorService.API}/connectors/${connectorId}/controller-services/${controllerServiceId}`,
+            { params }
+        );
+    }
+
     getConnectorParameterContext(
         connectorId: string,
         processGroupId: string
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/bind-go-to-service.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/bind-go-to-service.ts
new file mode 100644
index 00000000000..cf6818cdd43
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/bind-go-to-service.ts
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Store } from '@ngrx/store';
+import { Observable } from 'rxjs';
+import { take, takeUntil } from 'rxjs/operators';
+import { HttpErrorResponse } from '@angular/common/http';
+import { ConnectorService } from '../../service/connector.service';
+import { ErrorHelper } from '../../../../service/error-helper.service';
+import { ErrorContextKey } from '../../../../state/error';
+import * as ErrorActions from '../../../../state/error/error.actions';
+import * as ConnectorCanvasActions from './connector-canvas.actions';
+
+interface GoToServiceDialogRef {
+    afterClosed(): Observable<unknown>;
+    close(): void;
+}
+
+/**
+ * Builds a `goToService` callback for a read-only property-table dialog: 
resolves the
+ * current Connector id from the route, fetches the target Controller Service 
via the
+ * connector-scoped endpoint (bypassing the Troubleshooting gate), then 
navigates to it
+ * and closes the dialog. The fetch is torn down if the dialog closes first.
+ *
+ * Shared between the connector canvas and controller-services effects so the 
two
+ * read-only dialogs cannot drift from each other.
+ */
+export function bindGoToService(
+    store: Store,
+    connectorService: ConnectorService,
+    errorHelper: ErrorHelper,
+    dialogRef: GoToServiceDialogRef,
+    connectorId$: Observable<string | null>,
+    errorContext: ErrorContextKey
+): (serviceId: string) => void {
+    return (serviceId: string) => {
+        connectorId$.pipe(take(1)).subscribe((connectorId) => {
+            if (!connectorId) {
+                store.dispatch(
+                    ErrorActions.addBannerError({
+                        errorContext: {
+                            errors: ['Unable to determine Connector id for 
navigation.'],
+                            context: errorContext
+                        }
+                    })
+                );
+                return;
+            }
+
+            connectorService
+                .getControllerService(connectorId, serviceId)
+                .pipe(takeUntil(dialogRef.afterClosed()))
+                .subscribe({
+                    next: (serviceEntity) => {
+                        store.dispatch(
+                            
ConnectorCanvasActions.navigateToControllerService({
+                                processGroupId: 
serviceEntity.component.parentGroupId,
+                                serviceId: serviceEntity.id
+                            })
+                        );
+                        dialogRef.close();
+                    },
+                    error: (errorResponse: HttpErrorResponse) => {
+                        store.dispatch(
+                            ErrorActions.addBannerError({
+                                errorContext: {
+                                    errors: 
[errorHelper.getErrorString(errorResponse)],
+                                    context: errorContext
+                                }
+                            })
+                        );
+                    }
+                });
+        });
+    };
+}
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 671c2dcd008..3c6567eccbf 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
@@ -647,6 +647,93 @@ describe('ConnectorCanvasEffects', () => {
             
expect(mockDialogRef.componentInstance.parameterContext).toBeUndefined();
             
expect(mockDialogRef.componentInstance.goToParameter).toBeUndefined();
         });
+
+        it('wires goToService to fetch via the connector-scoped API and 
navigate to the controller service', async () => {
+            const { effects, actions$, store, mockConnectorService, 
mockDialogRef } = await setup();
+            const dispatchSpy = vi.spyOn(store, 'dispatch');
+            mockConnectorService.getControllerService = vi
+                .fn()
+                .mockReturnValue(of({ id: 'svc-1', component: { parentGroupId: 
'pg-cs', id: 'svc-1' } }));
+            mockDialogRef.close = vi.fn();
+
+            actions$(
+                of(
+                    viewComponentConfiguration({
+                        request: { entity: baseEntity, componentType: 
ComponentType.Processor }
+                    })
+                )
+            );
+            await firstValueFrom(effects.viewComponentConfiguration$);
+
+            
expect(mockDialogRef.componentInstance.goToService).toBeInstanceOf(Function);
+            mockDialogRef.componentInstance.goToService('svc-1');
+
+            
expect(mockConnectorService.getControllerService).toHaveBeenCalledWith('connector-1',
 'svc-1');
+            expect(mockDialogRef.close).toHaveBeenCalled();
+            expect(dispatchSpy).toHaveBeenCalledWith(
+                navigateToControllerService({
+                    processGroupId: 'pg-cs',
+                    serviceId: 'svc-1'
+                })
+            );
+        });
+
+        it('dispatches a banner error when goToService fails to fetch the 
controller service', async () => {
+            const { effects, actions$, store, mockConnectorService, 
mockDialogRef, mockErrorHelper } = await setup();
+            const dispatchSpy = vi.spyOn(store, 'dispatch');
+            const errorResponse = new HttpErrorResponse({ status: 404, 
statusText: 'Not Found' });
+            mockConnectorService.getControllerService = 
vi.fn().mockReturnValue(throwError(() => errorResponse));
+
+            actions$(
+                of(
+                    viewComponentConfiguration({
+                        request: { entity: baseEntity, componentType: 
ComponentType.Processor }
+                    })
+                )
+            );
+            await firstValueFrom(effects.viewComponentConfiguration$);
+
+            mockDialogRef.componentInstance.goToService('svc-missing');
+
+            
expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(errorResponse);
+            expect(dispatchSpy).toHaveBeenCalledWith(
+                ErrorActions.addBannerError({
+                    errorContext: {
+                        errors: ['Error message'],
+                        context: ErrorContextKey.CONNECTOR_CANVAS
+                    }
+                })
+            );
+        });
+
+        it('dispatches a banner error when goToService cannot resolve the 
connector id', async () => {
+            const { effects, actions$, store, mockConnectorService, 
mockDialogRef } = await setup({
+                connectorId: null
+            });
+            const dispatchSpy = vi.spyOn(store, 'dispatch');
+            mockConnectorService.getControllerService = vi.fn();
+
+            actions$(
+                of(
+                    viewComponentConfiguration({
+                        request: { entity: baseEntity, componentType: 
ComponentType.Processor }
+                    })
+                )
+            );
+            await firstValueFrom(effects.viewComponentConfiguration$);
+
+            mockDialogRef.componentInstance.goToService('svc-1');
+
+            
expect(mockConnectorService.getControllerService).not.toHaveBeenCalled();
+            expect(dispatchSpy).toHaveBeenCalledWith(
+                ErrorActions.addBannerError({
+                    errorContext: {
+                        errors: ['Unable to determine Connector id for 
navigation.'],
+                        context: ErrorContextKey.CONNECTOR_CANVAS
+                    }
+                })
+            );
+        });
     });
 
     describe('navigateToProvenanceForComponent$', () => {
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 c8cec7a5804..f75e4481e4d 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
@@ -65,6 +65,7 @@ import { EditRemoteProcessGroup } from 
'../../../../ui/common/component-dialogs/
 import * as ConnectorCanvasActions from './connector-canvas.actions';
 import * as ConnectorControllerServicesActions from 
'../connector-controller-services/connector-controller-services.actions';
 import { bindConnectorParameterContext } from 
'./bind-connector-parameter-context';
+import { bindGoToService } from './bind-go-to-service';
 import { SelectedComponent } from './connector-canvas.actions';
 import * as EmptyQueueActions from 
'../../../../state/empty-queue/empty-queue.actions';
 import {
@@ -720,7 +721,14 @@ export class ConnectorCanvasEffects {
         instance.createNewProperty = () => NEVER;
         instance.createNewService = () => NEVER;
         instance.convertToParameter = () => NEVER;
-        instance.goToService = () => undefined;
+        instance.goToService = bindGoToService(
+            this.store,
+            this.connectorService,
+            this.errorHelper,
+            dialogRef,
+            this.store.select(selectConnectorIdFromRoute),
+            ErrorContextKey.CONNECTOR_CANVAS
+        );
 
         // EditProcessor only exposes `parameterContext`; the underlying 
property table
         // disables parameter affordances on its own when parameterContext is 
undefined.
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.spec.ts
index 4b13e76b082..3345e3f8f86 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.spec.ts
@@ -31,6 +31,8 @@ import {
     selectConnectorControllerService
 } from './connector-controller-services.actions';
 import { selectConnectorParameterContext } from 
'../connector-canvas/connector-canvas.selectors';
+import { navigateToControllerService } from 
'../connector-canvas/connector-canvas.actions';
+import { selectConnectorIdFromRoute } from 
'./connector-controller-services.selectors';
 import { ConnectorService } from '../../service/connector.service';
 import { ErrorHelper } from '../../../../service/error-helper.service';
 import { ErrorContextKey } from '../../../../state/error';
@@ -38,6 +40,8 @@ import * as ErrorActions from 
'../../../../state/error/error.actions';
 import { ControllerServiceEntity, ParameterContextEntity } from 
'../../../../state/shared';
 import { createParameterContextFixture } from 
'../../testing/parameter-context-fixture';
 import { EditControllerService } from 
'../../../../ui/common/controller-service/edit-controller-service/edit-controller-service.component';
+import { HttpErrorResponse } from '@angular/common/http';
+import { MockStore } from '@ngrx/store/testing';
 
 function buildService(overrides: Partial<ControllerServiceEntity> = {}): 
ControllerServiceEntity {
     return {
@@ -51,6 +55,7 @@ function buildService(overrides: 
Partial<ControllerServiceEntity> = {}): Control
 describe('ConnectorControllerServicesEffects', () => {
     interface SetupOptions {
         parameterContext?: ParameterContextEntity | null;
+        connectorId?: string | null;
     }
 
     async function setup(options: SetupOptions = {}) {
@@ -58,7 +63,8 @@ describe('ConnectorControllerServicesEffects', () => {
 
         const mockConnectorService = {
             getConnectorControllerServices: vi.fn(),
-            getConnectorFlow: vi.fn()
+            getConnectorFlow: vi.fn(),
+            getControllerService: vi.fn()
         };
 
         const mockErrorHelper = {
@@ -78,11 +84,13 @@ describe('ConnectorControllerServicesEffects', () => {
             parameterContext: undefined as any,
             supportsParameters: true as any
         };
+        const mockDialogRef = {
+            componentInstance: mockDialogInstance,
+            afterClosed: () => afterClosed$.asObservable(),
+            close: vi.fn()
+        };
         const mockDialog = {
-            open: vi.fn().mockReturnValue({
-                componentInstance: mockDialogInstance,
-                afterClosed: () => afterClosed$.asObservable()
-            })
+            open: vi.fn().mockReturnValue(mockDialogRef)
         };
 
         await TestBed.configureTestingModule({
@@ -94,6 +102,10 @@ describe('ConnectorControllerServicesEffects', () => {
                         {
                             selector: selectConnectorParameterContext,
                             value: options.parameterContext ?? null
+                        },
+                        {
+                            selector: selectConnectorIdFromRoute,
+                            value: options.connectorId !== undefined ? 
options.connectorId : 'conn-1'
                         }
                     ]
                 }),
@@ -106,10 +118,13 @@ describe('ConnectorControllerServicesEffects', () => {
 
         return {
             effects: TestBed.inject(ConnectorControllerServicesEffects),
+            store: TestBed.inject(MockStore),
             mockConnectorService,
+            mockErrorHelper,
             mockRouter,
             mockDialog,
             mockDialogInstance,
+            mockDialogRef,
             afterClosed$,
             actions$: (stream: Observable<Action>) => {
                 actions$ = stream;
@@ -256,5 +271,73 @@ describe('ConnectorControllerServicesEffects', () => {
             expect(mockDialogInstance.supportsParameters).toBe(false);
             expect(mockDialogInstance.goToParameter).toBeUndefined();
         });
+
+        it('should wire goToService to fetch via the connector-scoped API and 
navigate to the controller service', async () => {
+            const { effects, actions$, store, mockConnectorService, 
mockDialogInstance, mockDialogRef } = await setup();
+            const dispatchSpy = vi.spyOn(store, 'dispatch');
+            mockConnectorService.getControllerService.mockReturnValue(
+                of({ id: 'svc-2', component: { parentGroupId: 'pg-cs', id: 
'svc-2' } })
+            );
+
+            actions$(of(openViewControllerServiceDialog({ controllerService: 
buildService() })));
+            await firstValueFrom(effects.openViewControllerServiceDialog$);
+
+            expect(mockDialogInstance.goToService).toBeInstanceOf(Function);
+            mockDialogInstance.goToService('svc-2');
+
+            
expect(mockConnectorService.getControllerService).toHaveBeenCalledWith('conn-1',
 'svc-2');
+            expect(mockDialogRef.close).toHaveBeenCalled();
+            expect(dispatchSpy).toHaveBeenCalledWith(
+                navigateToControllerService({
+                    processGroupId: 'pg-cs',
+                    serviceId: 'svc-2'
+                })
+            );
+        });
+
+        it('should dispatch a banner error when goToService fails to fetch the 
controller service', async () => {
+            const { effects, actions$, store, mockConnectorService, 
mockDialogInstance, mockErrorHelper } =
+                await setup();
+            const dispatchSpy = vi.spyOn(store, 'dispatch');
+            const errorResponse = new HttpErrorResponse({ status: 404, 
statusText: 'Not Found' });
+            
mockConnectorService.getControllerService.mockReturnValue(throwError(() => 
errorResponse));
+
+            actions$(of(openViewControllerServiceDialog({ controllerService: 
buildService() })));
+            await firstValueFrom(effects.openViewControllerServiceDialog$);
+
+            mockDialogInstance.goToService('svc-missing');
+
+            
expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(errorResponse);
+            expect(dispatchSpy).toHaveBeenCalledWith(
+                ErrorActions.addBannerError({
+                    errorContext: {
+                        errors: ['Error message'],
+                        context: ErrorContextKey.CONTROLLER_SERVICES
+                    }
+                })
+            );
+        });
+
+        it('should dispatch a banner error when goToService cannot resolve the 
connector id', async () => {
+            const { effects, actions$, store, mockConnectorService, 
mockDialogInstance } = await setup({
+                connectorId: null
+            });
+            const dispatchSpy = vi.spyOn(store, 'dispatch');
+
+            actions$(of(openViewControllerServiceDialog({ controllerService: 
buildService() })));
+            await firstValueFrom(effects.openViewControllerServiceDialog$);
+
+            mockDialogInstance.goToService('svc-1');
+
+            
expect(mockConnectorService.getControllerService).not.toHaveBeenCalled();
+            expect(dispatchSpy).toHaveBeenCalledWith(
+                ErrorActions.addBannerError({
+                    errorContext: {
+                        errors: ['Unable to determine Connector id for 
navigation.'],
+                        context: ErrorContextKey.CONTROLLER_SERVICES
+                    }
+                })
+            );
+        });
     });
 });
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.ts
index cfe655e33d9..05d870af659 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.ts
@@ -30,7 +30,9 @@ import { ErrorHelper } from 
'../../../../service/error-helper.service';
 import { EditControllerService } from 
'../../../../ui/common/controller-service/edit-controller-service/edit-controller-service.component';
 import { EditControllerServiceDialogRequest } from '../../../../state/shared';
 import * as ConnectorControllerServicesActions from 
'./connector-controller-services.actions';
+import { selectConnectorIdFromRoute } from 
'./connector-controller-services.selectors';
 import { bindConnectorParameterContext } from 
'../connector-canvas/bind-connector-parameter-context';
+import { bindGoToService } from '../connector-canvas/bind-go-to-service';
 
 @Injectable()
 export class ConnectorControllerServicesEffects {
@@ -148,7 +150,14 @@ export class ConnectorControllerServicesEffects {
                     // values still render in the value tip.
                     instance.createNewService = () => NEVER;
                     instance.convertToParameter = () => NEVER;
-                    instance.goToService = () => undefined;
+                    instance.goToService = bindGoToService(
+                        this.store,
+                        this.connectorService,
+                        this.errorHelper,
+                        dialogRef,
+                        this.store.select(selectConnectorIdFromRoute),
+                        ErrorContextKey.CONTROLLER_SERVICES
+                    );
 
                     bindConnectorParameterContext(
                         this.store,
diff --git 
a/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/ConnectorClient.java
 
b/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/ConnectorClient.java
index 607548c3a8b..e1327ca484c 100644
--- 
a/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/ConnectorClient.java
+++ 
b/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/ConnectorClient.java
@@ -24,6 +24,7 @@ import org.apache.nifi.web.api.entity.ConfigurationStepEntity;
 import org.apache.nifi.web.api.entity.ConfigurationStepNamesEntity;
 import org.apache.nifi.web.api.entity.ConnectorEntity;
 import org.apache.nifi.web.api.entity.ConnectorPropertyAllowableValuesEntity;
+import org.apache.nifi.web.api.entity.ControllerServiceEntity;
 import org.apache.nifi.web.api.entity.DropRequestEntity;
 import org.apache.nifi.web.api.entity.MigrationPayloadEntity;
 import org.apache.nifi.web.api.entity.MigrationRequestEntity;
@@ -470,6 +471,17 @@ public interface ConnectorClient {
      */
     ComponentStateEntity clearProcessorState(String connectorId, String 
processorId) throws NiFiClientException, IOException;
 
+    /**
+     * Gets a controller service within a connector.
+     *
+     * @param connectorId the connector ID
+     * @param controllerServiceId the controller service ID
+     * @return the controller service entity
+     * @throws NiFiClientException if an error occurs during the request
+     * @throws IOException if an I/O error occurs
+     */
+    ControllerServiceEntity getControllerService(String connectorId, String 
controllerServiceId) throws NiFiClientException, IOException;
+
     /**
      * Gets the state for a controller service within a connector.
      *
diff --git 
a/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/impl/JerseyConnectorClient.java
 
b/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/impl/JerseyConnectorClient.java
index 751b22d3a46..30abe262863 100644
--- 
a/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/impl/JerseyConnectorClient.java
+++ 
b/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/impl/JerseyConnectorClient.java
@@ -34,6 +34,7 @@ import 
org.apache.nifi.web.api.entity.ConfigurationStepNamesEntity;
 import org.apache.nifi.web.api.entity.ConnectorEntity;
 import org.apache.nifi.web.api.entity.ConnectorPropertyAllowableValuesEntity;
 import org.apache.nifi.web.api.entity.ConnectorRunStatusEntity;
+import org.apache.nifi.web.api.entity.ControllerServiceEntity;
 import org.apache.nifi.web.api.entity.DropRequestEntity;
 import org.apache.nifi.web.api.entity.MigrationPayloadEntity;
 import org.apache.nifi.web.api.entity.MigrationRequestEntity;
@@ -844,6 +845,25 @@ public class JerseyConnectorClient extends 
AbstractJerseyClient implements Conne
         });
     }
 
+    @Override
+    public ControllerServiceEntity getControllerService(final String 
connectorId, final String controllerServiceId) throws NiFiClientException, 
IOException {
+        if (StringUtils.isBlank(connectorId)) {
+            throw new IllegalArgumentException("Connector id cannot be null or 
blank");
+        }
+        if (StringUtils.isBlank(controllerServiceId)) {
+            throw new IllegalArgumentException("Controller service id cannot 
be null or blank");
+        }
+
+        return executeAction("Error retrieving controller service for 
Connector " + connectorId, () -> {
+            final WebTarget target = connectorTarget
+                .path("/controller-services/{controllerServiceId}")
+                .resolveTemplate("id", connectorId)
+                .resolveTemplate("controllerServiceId", controllerServiceId);
+
+            return 
getRequestBuilder(target).get(ControllerServiceEntity.class);
+        });
+    }
+
     @Override
     public ComponentStateEntity getControllerServiceState(final String 
connectorId, final String controllerServiceId) throws NiFiClientException, 
IOException {
         if (StringUtils.isBlank(connectorId)) {

Reply via email to