You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by jo...@apache.org on 2022/04/20 14:55:07 UTC

[nifi] 04/04: NIFI-9776 Adding the possibility to export flow definition with referenced services (#5859)

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

joewitt pushed a commit to branch support/nifi-1.16
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit 04a309af1763747ab9e832d7225df4c3fee0c0bd
Author: simonbence <61...@users.noreply.github.com>
AuthorDate: Wed Apr 20 16:29:38 2022 +0200

    NIFI-9776 Adding the possibility to export flow definition with referenced services (#5859)
    
    * NIFI-9776 Adding the possibility to export flow definition with referenced services
    
    * NIFI-9776 Refining naming based on code review
---
 nifi-docs/src/main/asciidoc/user-guide.adoc        |  6 +-
 .../org/apache/nifi/web/NiFiServiceFacade.java     | 10 ++++
 .../apache/nifi/web/StandardNiFiServiceFacade.java | 51 ++++++++++++++++-
 .../apache/nifi/web/api/ProcessGroupResource.java  | 17 +++++-
 .../nifi/web/StandardNiFiServiceFacadeTest.java    | 64 ++++++++++++++++++++++
 .../nifi/web/api/TestProcessGroupResource.java     |  2 +-
 .../src/main/webapp/js/nf/canvas/nf-actions.js     | 18 +++++-
 .../main/webapp/js/nf/canvas/nf-context-menu.js    |  5 +-
 8 files changed, 162 insertions(+), 11 deletions(-)

diff --git a/nifi-docs/src/main/asciidoc/user-guide.adoc b/nifi-docs/src/main/asciidoc/user-guide.adoc
index 3024878c2c..9b6286b40f 100644
--- a/nifi-docs/src/main/asciidoc/user-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/user-guide.adoc
@@ -372,13 +372,15 @@ NOTE: It is also possible to double-click on the Process Group to enter it.
 - *View connections->Downstream*: This option allows the user to see and "jump to" downstream connections that are going out of the Process Group.
 - *Center in view*: This option centers the view of the canvas on the given Process Group.
 - *Group*: This option allows the user to create a new Process Group that contains the selected Process Group and any other components selected on the canvas.
-- *Download flow definition*: This option allows the user to download the flow definition of the process group as a JSON file. The file can be used as a backup or imported into a link:https://nifi.apache.org/registry.html[NiFi Registry^] using the <<toolkit-guide.adoc#nifi_CLI,NiFi CLI>>. (Note: If "Download flow definition" is selected for a versioned process group, there is no versioning information in the download. In other words, the resulting contents of the JSON file is the same wh [...]
+- *Download flow definition*: This option allows the user to download the flow definition of the process group as a JSON file. The file can be used as a backup or imported into a link:https://nifi.apache.org/registry.html[NiFi Registry^] using the <<toolkit-guide.adoc#nifi_CLI,NiFi CLI>>. There are two options when downloading a flow definition:
+** -> *Without external services*: Controller services referenced by the selected process group but outside its scope (e.g., services in a parent group) _will not be_ included in the flow definition as services.
+** -> *With external services*: Controller services referenced by the selected process group but outside its scope (e.g., services in a parent group) _will be_ included in the flow definition.
 - *Create template*: This option allows the user to create a template from the selected Process Group.
 - *Copy*: This option places a copy of the selected Process Group on the clipboard, so that it may be pasted elsewhere on the canvas by right-clicking on the canvas and selecting Paste. The Copy/Paste actions also may be done using the keystrokes Ctrl-C (Command-C) and Ctrl-V (Command-V).
 - *Empty all queues*: This option allows the user to empty all queues in the selected Process Group. All FlowFiles from all connections waiting at the time of the request will be removed.
 - *Delete*: This option allows the DFM to delete a Process Group.
 
-
+(Note: If "Download flow definition" is selected for a versioned process group, there is no versioning information in the download. In other words, the resulting contents of the JSON file is the same whether the process group is versioned or not.)
 
 [[remote_process_group]]
 image:iconRemoteProcessGroup.png["Remote Process Group", width=32]
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index 1bbc4d9326..ea78af85ae 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -1624,6 +1624,16 @@ public interface NiFiServiceFacade {
      */
     VersionedFlowSnapshot getCurrentFlowSnapshotByGroupId(String processGroupId);
 
+    /**
+     * Get the current state of the Process Group with the given ID, converted to a Versioned Flow Snapshot. Controller
+     * Services referenced by the Components contained by the Process Group but are part of the parent Process Group(s)
+     * will be included and will be considered as part of the requested Process Group.
+     *
+     * @param processGroupId the ID of the Process Group
+     * @return the current Process Group converted to a Versioned Flow Snapshot for download
+     */
+    VersionedFlowSnapshot getCurrentFlowSnapshotByGroupIdWithReferencedControllerServices(String processGroupId);
+
     /**
      * Returns the name of the Flow Registry that is registered with the given ID. If no Flow Registry exists with the given ID, will return
      * the ID itself as the name
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 066b1310f2..4cafa8429f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -4682,6 +4682,23 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
     @Override
     public VersionedFlowSnapshot getCurrentFlowSnapshotByGroupId(final String processGroupId) {
+        return getCurrentFlowSnapshotByGroupId(processGroupId, false);
+    }
+
+    @Override
+    public VersionedFlowSnapshot getCurrentFlowSnapshotByGroupIdWithReferencedControllerServices(final String processGroupId) {
+        return getCurrentFlowSnapshotByGroupId(processGroupId, true);
+    }
+
+    private Set<String> getAllSubGroups(ProcessGroup processGroup) {
+        final Set<String> result = processGroup.findAllProcessGroups().stream()
+                .map(ProcessGroup::getIdentifier)
+                .collect(Collectors.toSet());
+        result.add(processGroup.getIdentifier());
+        return result;
+    }
+
+    private VersionedFlowSnapshot getCurrentFlowSnapshotByGroupId(final String processGroupId, final boolean includeReferencedControllerServices) {
         final ProcessGroup processGroup = processGroupDAO.getProcessGroup(processGroupId);
 
         // Create a complete (include descendant flows) VersionedProcessGroup snapshot of the flow as it is
@@ -4691,12 +4708,40 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
                 mapper.mapNonVersionedProcessGroup(processGroup, controllerFacade.getControllerServiceProvider());
 
         // Create a complete (include descendant flows) map of parameter contexts
-        final Map<String, VersionedParameterContext> parameterContexts =
-                mapper.mapParameterContexts(processGroup, true);
+        final Map<String, VersionedParameterContext> parameterContexts = mapper.mapParameterContexts(processGroup, true);
 
+        final Map<String, ExternalControllerServiceReference> externalControllerServiceReferences =
+                Optional.ofNullable(nonVersionedProcessGroup.getExternalControllerServiceReferences()).orElse(Collections.emptyMap());
+        final Set<VersionedControllerService> controllerServices = new HashSet<>(nonVersionedProcessGroup.getControllerServices());
         final VersionedFlowSnapshot nonVersionedFlowSnapshot = new VersionedFlowSnapshot();
+
+        ProcessGroup parentGroup = processGroup.getParent();
+
+        if (includeReferencedControllerServices && parentGroup != null) {
+            final Set<VersionedControllerService> externalServices = new HashSet<>();
+
+            do {
+                final Set<ControllerServiceNode> controllerServiceNodes = parentGroup.getControllerServices(false);
+
+                for (final ControllerServiceNode controllerServiceNode : controllerServiceNodes) {
+                    final VersionedControllerService versionedControllerService =
+                            mapper.mapControllerService(controllerServiceNode, controllerFacade.getControllerServiceProvider(), getAllSubGroups(processGroup), externalControllerServiceReferences);
+
+                    if (externalControllerServiceReferences.keySet().contains(versionedControllerService.getIdentifier())) {
+                        versionedControllerService.setGroupIdentifier(processGroupId);
+                        externalServices.add(versionedControllerService);
+                    }
+                }
+            } while ((parentGroup = parentGroup.getParent()) != null);
+
+            controllerServices.addAll(externalServices);
+            nonVersionedFlowSnapshot.setExternalControllerServices(new HashMap<>());
+        } else {
+            nonVersionedFlowSnapshot.setExternalControllerServices(externalControllerServiceReferences);
+        }
+
+        nonVersionedProcessGroup.setControllerServices(controllerServices);
         nonVersionedFlowSnapshot.setFlowContents(nonVersionedProcessGroup);
-        nonVersionedFlowSnapshot.setExternalControllerServices(nonVersionedProcessGroup.getExternalControllerServiceReferences());
         nonVersionedFlowSnapshot.setParameterContexts(parameterContexts);
         nonVersionedFlowSnapshot.setFlowEncodingVersion(RestBasedFlowRegistry.FLOW_ENCODING_VERSION);
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index 1c946c72ba..146631613f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -349,7 +349,18 @@ public class ProcessGroupResource extends FlowUpdateResource<ProcessGroupImportE
         @ApiResponse(code = 404, message = "The specified resource could not be found."),
         @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
     })
-    public Response exportProcessGroup(@ApiParam(value = "The process group id.", required = true) @PathParam("id") final String groupId) {
+    public Response exportProcessGroup(
+            @ApiParam(
+                    value = "The process group id.",
+                    required = true
+            )
+            @PathParam("id") final String groupId,
+            @ApiParam(
+                    value = "If referenced services from outside the target group should be included",
+                    required = false
+            )
+            @QueryParam("includeReferencedServices")
+            @DefaultValue("false") boolean includeReferencedServices) {
         // authorize access
         serviceFacade.authorizeAccess(lookup -> {
             // ensure access to process groups (nested), encapsulated controller services and referenced parameter contexts
@@ -359,7 +370,9 @@ public class ProcessGroupResource extends FlowUpdateResource<ProcessGroupImportE
         });
 
         // get the versioned flow
-        final VersionedFlowSnapshot currentVersionedFlowSnapshot = serviceFacade.getCurrentFlowSnapshotByGroupId(groupId);
+        final VersionedFlowSnapshot currentVersionedFlowSnapshot = includeReferencedServices
+            ? serviceFacade.getCurrentFlowSnapshotByGroupIdWithReferencedControllerServices(groupId)
+            : serviceFacade.getCurrentFlowSnapshotByGroupId(groupId);
 
         // determine the name of the attachment - possible issues with spaces in file names
         final VersionedProcessGroup currentVersionedProcessGroup = currentVersionedFlowSnapshot.getFlowContents();
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
index 87d80c1a24..50355e69ee 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
@@ -34,7 +34,9 @@ import org.apache.nifi.authorization.user.NiFiUserDetails;
 import org.apache.nifi.authorization.user.StandardNiFiUser.Builder;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.flow.FlowManager;
+import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.flow.VersionedControllerService;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.groups.RemoteProcessGroup;
 import org.apache.nifi.history.History;
@@ -71,10 +73,12 @@ import org.springframework.security.core.context.SecurityContextHolder;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -84,6 +88,7 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.mock;
@@ -91,6 +96,9 @@ import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.anySet;
+import static org.mockito.Mockito.anyMap;
 
 public class StandardNiFiServiceFacadeTest {
 
@@ -381,6 +389,62 @@ public class StandardNiFiServiceFacadeTest {
         assertNull(versionedFlowSnapshot.getSnapshotMetadata());
     }
 
+    @Test
+    public void testGetCurrentFlowSnapshotByGroupIdWithReferencedControllerServices() {
+        final String groupId = UUID.randomUUID().toString();
+        final ProcessGroup processGroup = mock(ProcessGroup.class);
+        final ProcessGroup parentProcessGroup = mock(ProcessGroup.class);
+
+        final Set<ControllerServiceNode> parentControllerServices = new HashSet<>();
+        final ControllerServiceNode parentControllerService1 = mock(ControllerServiceNode.class);
+        final ControllerServiceNode parentControllerService2 = mock(ControllerServiceNode.class);
+        parentControllerServices.add(parentControllerService1);
+        parentControllerServices.add(parentControllerService2);
+
+        when(processGroupDAO.getProcessGroup(groupId)).thenReturn(processGroup);
+        when(processGroup.getParent()).thenReturn(parentProcessGroup);
+        when(parentProcessGroup.getControllerServices(anyBoolean())).thenReturn(parentControllerServices);
+
+        final FlowManager flowManager = mock(FlowManager.class);
+        final ExtensionManager extensionManager = mock(ExtensionManager.class);
+        when(flowController.getFlowManager()).thenReturn(flowManager);
+        when(flowController.getExtensionManager()).thenReturn(extensionManager);
+
+        final ControllerServiceProvider controllerServiceProvider = mock(ControllerServiceProvider.class);
+        when(flowController.getControllerServiceProvider()).thenReturn(controllerServiceProvider);
+
+        final VersionControlInformation versionControlInformation = mock(VersionControlInformation.class);
+        when(processGroup.getVersionControlInformation()).thenReturn(versionControlInformation);
+
+        // use spy to mock the make() method for generating a new flow mapper to make this testable
+        final StandardNiFiServiceFacade serviceFacadeSpy = spy(serviceFacade);
+        final NiFiRegistryFlowMapper flowMapper = mock(NiFiRegistryFlowMapper.class);
+        when(serviceFacadeSpy.makeNiFiRegistryFlowMapper(extensionManager)).thenReturn(flowMapper);
+
+        final InstantiatedVersionedProcessGroup nonVersionedProcessGroup = spy(new InstantiatedVersionedProcessGroup(UUID.randomUUID().toString(), UUID.randomUUID().toString()));
+        when(flowMapper.mapNonVersionedProcessGroup(processGroup, controllerServiceProvider)).thenReturn(nonVersionedProcessGroup);
+
+        final VersionedControllerService versionedControllerService1 = mock(VersionedControllerService.class);
+        final VersionedControllerService versionedControllerService2 = mock(VersionedControllerService.class);
+
+        Mockito.when(versionedControllerService1.getIdentifier()).thenReturn("test");
+        Mockito.when(versionedControllerService2.getIdentifier()).thenReturn("test2");
+
+        when(flowMapper.mapControllerService(same(parentControllerService1), same(controllerServiceProvider), anySet(), anyMap())).thenReturn(versionedControllerService1);
+        when(flowMapper.mapControllerService(same(parentControllerService2), same(controllerServiceProvider), anySet(), anyMap())).thenReturn(versionedControllerService2);
+        when(flowMapper.mapParameterContexts(processGroup, true)).thenReturn(new HashMap<>());
+
+        final ExternalControllerServiceReference externalControllerServiceReference = mock(ExternalControllerServiceReference.class);
+        final Map<String, ExternalControllerServiceReference> externalControllerServiceReferences = new LinkedHashMap<>();
+        externalControllerServiceReferences.put("test", externalControllerServiceReference);
+        when(nonVersionedProcessGroup.getExternalControllerServiceReferences()).thenReturn(externalControllerServiceReferences);
+
+        final VersionedFlowSnapshot versionedFlowSnapshot = serviceFacadeSpy.getCurrentFlowSnapshotByGroupIdWithReferencedControllerServices(groupId);
+
+        assertEquals(1, versionedFlowSnapshot.getFlowContents().getControllerServices().size());
+        assertEquals("test", versionedFlowSnapshot.getFlowContents().getControllerServices().iterator().next().getIdentifier());
+    }
+
     @Test
     public void testIsAnyProcessGroupUnderVersionControl_None() {
         final String groupId = UUID.randomUUID().toString();
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestProcessGroupResource.java
index cc74c3dd26..ed28dfe72e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestProcessGroupResource.java
@@ -53,7 +53,7 @@ public class TestProcessGroupResource {
         when(versionedFlowSnapshot.getFlowContents()).thenReturn(versionedProcessGroup);
         when(versionedProcessGroup.getName()).thenReturn(flowName);
 
-        final Response response = processGroupResource.exportProcessGroup(groupId);
+        final Response response = processGroupResource.exportProcessGroup(groupId, false);
 
         final VersionedFlowSnapshot resultEntity = (VersionedFlowSnapshot)response.getEntity();
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
index ae9345dd3d..d256a9c615 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
@@ -1718,10 +1718,24 @@
             }
         },
 
+        /**
+         * Downloads the current flow, without including the external Controller Services
+         */
+        downloadFlowWithoutExternalServices: function (selection) {
+            this.downloadFlow(selection, false);
+        },
+
+        /**
+         * Downloads the current flow, including the external Controller Services
+         */
+        downloadFlowWithExternalServices: function (selection) {
+            this.downloadFlow(selection, true);
+        },
+
         /**
          * Downloads the current flow
          */
-        downloadFlow: function (selection) {
+        downloadFlow: function (selection,includeReferencedServices) {
             var processGroupId = null;
 
             if (selection.empty()) {
@@ -1737,7 +1751,7 @@
                 var parameters = {};
 
                 // open the url
-                var uri = '../nifi-api/process-groups/' + encodeURIComponent(processGroupId) + '/download';
+                var uri = '../nifi-api/process-groups/' + encodeURIComponent(processGroupId) + '/download?includeReferencedServices=' + includeReferencedServices;
                 window.open(uri);
             }
         },
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
index fd9ec47664..bd0460ecb0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
@@ -861,7 +861,10 @@
         {id: 'move-into-parent-menu-item', condition: canMoveToParent, menuItem: {clazz: 'fa fa-arrows', text: 'Move to parent group', action: 'moveIntoParent'}},
         {id: 'group-menu-item', condition: canGroup, menuItem: {clazz: 'icon icon-group', text: 'Group', action: 'group'}},
         {separator: true},
-        {id: 'download-menu-item', condition: supportsDownloadFlow, menuItem: {clazz: 'fa', text: 'Download flow definition', action: 'downloadFlow'}},
+        {id: 'download-menu-item', groupMenuItem: {clazz: 'fa', text: 'Download flow definition'}, menuItems: [
+            {id: 'download-menu-item-without', condition: hasUpstream, menuItem: {clazz: 'fa', text: 'Without external services', action: 'downloadFlowWithoutExternalServices'}},
+            {id: 'download-menu-item-with', condition: hasDownstream, menuItem: {clazz: 'fa', text: 'With external services', action: 'downloadFlowWithExternalServices'}}
+        ]},
         {separator: true},
         {id: 'upload-template-menu-item', condition: canUploadTemplate, menuItem: {clazz: 'icon icon-template-import', text: 'Upload template', action: 'uploadTemplate'}},
         {id: 'template-menu-item', condition: canCreateTemplate, menuItem: {clazz: 'icon icon-template-save', text: 'Create template', action: 'template'}},