You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ri...@apache.org on 2020/02/05 21:16:00 UTC

[incubator-streampipes] branch dev updated: STREAMPIPES-70: Auto-save pipelines in pipeline development mode

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

riemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/incubator-streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new f5d713d  STREAMPIPES-70: Auto-save pipelines in pipeline development mode
     new 1d1f628  Merge branch 'dev' of github.com:apache/incubator-streampipes into dev
f5d713d is described below

commit f5d713d42ad8aa1253405e22eb63224778ebd01e
Author: Dominik Riemer <ri...@fzi.de>
AuthorDate: Wed Feb 5 22:15:28 2020 +0100

    STREAMPIPES-70: Auto-save pipelines in pipeline development mode
---
 .../backend/StreamPipesResourceConfig.java         |  8 ++-
 .../streampipes/rest/api/IPipelineCache.java       | 26 ++++-----
 .../streampipes/rest/impl/PipelineCache.java       | 65 ++++++++++++++++++++++
 .../pipeline-assembly.controller.ts                | 33 +++++++++--
 .../pipeline-assembly/pipeline-assembly.tmpl.html  | 15 ++++-
 .../pipeline-element-options.controller.ts         |  9 ++-
 .../components/pipeline/pipeline.component.ts      |  4 +-
 .../components/pipeline/pipeline.controller.ts     | 32 ++++++++++-
 .../save-pipeline/save-pipeline.controller.ts      |  1 +
 .../preview/pipeline-preview.controller.ts         |  2 +-
 .../app/services/pipeline-positioning.service.ts   | 56 ++++++++++---------
 ui/src/app/services/rest-api.service.ts            | 12 ++++
 ui/src/scss/main.scss                              |  1 +
 ui/src/scss/sp/pipeline-assembly.scss              | 12 ++++
 14 files changed, 221 insertions(+), 55 deletions(-)

diff --git a/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java b/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java
index 3863855..ea63713 100644
--- a/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java
+++ b/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java
@@ -18,9 +18,6 @@
 
 package org.apache.streampipes.backend;
 
-import org.glassfish.jersey.media.multipart.MultiPartFeature;
-import org.glassfish.jersey.server.ResourceConfig;
-import org.springframework.context.annotation.Configuration;
 import org.apache.streampipes.rest.impl.ApplicationLink;
 import org.apache.streampipes.rest.impl.AssetDashboard;
 import org.apache.streampipes.rest.impl.Authentication;
@@ -36,6 +33,7 @@ import org.apache.streampipes.rest.impl.OntologyContext;
 import org.apache.streampipes.rest.impl.OntologyKnowledge;
 import org.apache.streampipes.rest.impl.OntologyMeasurementUnit;
 import org.apache.streampipes.rest.impl.OntologyPipelineElement;
+import org.apache.streampipes.rest.impl.PipelineCache;
 import org.apache.streampipes.rest.impl.PipelineCategory;
 import org.apache.streampipes.rest.impl.PipelineElementAsset;
 import org.apache.streampipes.rest.impl.PipelineElementCategory;
@@ -63,6 +61,9 @@ import org.apache.streampipes.rest.shared.serializer.GsonClientModelProvider;
 import org.apache.streampipes.rest.shared.serializer.GsonWithIdProvider;
 import org.apache.streampipes.rest.shared.serializer.GsonWithoutIdProvider;
 import org.apache.streampipes.rest.shared.serializer.JsonLdProvider;
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.springframework.context.annotation.Configuration;
 
 import javax.ws.rs.ApplicationPath;
 
@@ -110,6 +111,7 @@ public class StreamPipesResourceConfig extends ResourceConfig {
     register(DataLakeNoUserResourceV3.class);
     register(PipelineElementFile.class);
     register(FileServingResource.class);
+    register(PipelineCache.class);
 
 
     // Serializers
diff --git a/ui/src/app/editor/components/pipeline/pipeline.component.ts b/streampipes-rest/src/main/java/org/apache/streampipes/rest/api/IPipelineCache.java
similarity index 66%
copy from ui/src/app/editor/components/pipeline/pipeline.component.ts
copy to streampipes-rest/src/main/java/org/apache/streampipes/rest/api/IPipelineCache.java
index e18b03a..bae7ce7 100644
--- a/ui/src/app/editor/components/pipeline/pipeline.component.ts
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/api/IPipelineCache.java
@@ -15,20 +15,16 @@
  * limitations under the License.
  *
  */
+package org.apache.streampipes.rest.api;
 
-import {PipelineController} from "./pipeline.controller";
-declare const require: any;
+import javax.ws.rs.core.Response;
 
-export let PipelineComponent = {
-    template: require('./pipeline.tmpl.html'),
-    bindings: {
-        staticProperty : "=",
-        rawPipelineModel: "=",
-        allElements: "=",
-        preview: "<",
-        canvasId: "@",
-        pipelineValid: "="
-    },
-    controller: PipelineController,
-    controllerAs: 'ctrl'
-};
+public interface IPipelineCache {
+
+  Response updateCachedPipeline(String user, String rawPipelineModel);
+
+  Response getCachedPipeline(String user);
+
+  Response removePipelineFromCache(String user);
+
+}
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCache.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCache.java
new file mode 100644
index 0000000..ca39af2
--- /dev/null
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCache.java
@@ -0,0 +1,65 @@
+/*
+ * 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.streampipes.rest.impl;
+
+import org.apache.streampipes.rest.api.IPipelineCache;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Path("/v2/users/{username}/pipeline-cache")
+public class PipelineCache extends AbstractRestInterface implements IPipelineCache {
+
+  private static ConcurrentHashMap<String, String> cachedPipelines = new ConcurrentHashMap<>();
+
+  @POST
+  @Produces(MediaType.APPLICATION_JSON)
+  @Override
+  public Response updateCachedPipeline(@PathParam("username") String user,
+                                       String rawPipelineModel) {
+    cachedPipelines.put(user, rawPipelineModel);
+    return ok();
+  }
+
+  @GET
+  @Produces(MediaType.APPLICATION_JSON)
+  @Override
+  public Response getCachedPipeline(@PathParam("username") String user) {
+    if (cachedPipelines.containsKey(user)) {
+      return ok(cachedPipelines.get(user));
+    } else {
+      return ok();
+    }
+  }
+
+  @DELETE
+  @Produces(MediaType.APPLICATION_JSON)
+  @Override
+  public Response removePipelineFromCache(@PathParam("username") String user) {
+    cachedPipelines.remove(user);
+    return ok();
+  }
+}
diff --git a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.controller.ts b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.controller.ts
index 1e87d11..791c3ba 100644
--- a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.controller.ts
+++ b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.controller.ts
@@ -17,6 +17,7 @@
  */
 
 import {PipelineValidationService} from "../../services/pipeline-validation.service";
+import {RestApi} from "../../../services/rest-api.service";
 
 export class PipelineAssemblyController {
 
@@ -34,7 +35,7 @@ export class PipelineAssemblyController {
     rawPipelineModel: any;
     PipelinePositioningService: any;
     PipelineValidationService: PipelineValidationService;
-    RestApi: any;
+    RestApi: RestApi;
     selectMode: any;
     currentPipelineName: any;
     currentPipelineDescription: any;
@@ -45,6 +46,9 @@ export class PipelineAssemblyController {
 
     pipelineValid: boolean = false;
 
+    pipelineCacheRunning: boolean = false;
+    pipelineCached: boolean = false;
+
     constructor(JsplumbBridge,
                 PipelinePositioningService,
                 EditorDialogManager,
@@ -89,6 +93,8 @@ export class PipelineAssemblyController {
     $onInit() {
         if (this.currentModifiedPipelineId) {
             this.displayPipelineById();
+        } else {
+            this.checkAndDisplayCachedPipeline();
         }
     }
 
@@ -151,6 +157,10 @@ export class PipelineAssemblyController {
         this.currentZoomLevel = 1;
         this.JsplumbBridge.setZoom(this.currentZoomLevel);
         this.JsplumbBridge.repaintEverything();
+        this.RestApi.removePipelineFromCache().then(msg => {
+            this.pipelineCached = false;
+            this.pipelineCacheRunning = false;
+        });
     };
 
     /**
@@ -173,6 +183,17 @@ export class PipelineAssemblyController {
         this.EditorDialogManager.showSavePipelineDialog(pipeline, modificationMode);
     }
 
+    checkAndDisplayCachedPipeline() {
+        this.RestApi.getCachedPipeline().then(msg => {
+            if (msg.data !== "") {
+                this.rawPipelineModel = msg.data;
+                this.$timeout(() => {
+                    this.displayPipelineInEditor(true);
+                });
+            }
+        });
+    }
+
     displayPipelineById() {
         this.RestApi.getPipelineById(this.currentModifiedPipelineId)
             .then((msg) => {
@@ -181,13 +202,17 @@ export class PipelineAssemblyController {
                 this.currentPipelineDescription = pipeline.description;
                 this.rawPipelineModel = this.JsplumbService.makeRawPipeline(pipeline, false);
                 this.$timeout(() => {
-                    this.PipelinePositioningService.displayPipeline(this.rawPipelineModel, "#assembly", false);
-                    this.TransitionService.makePipelineAssemblyEmpty(false);
-                    this.pipelineValid = this.PipelineValidationService.isValidPipeline(this.rawPipelineModel);
+                    this.displayPipelineInEditor(true);
                 });
             });
     };
 
+    displayPipelineInEditor(autoLayout) {
+        this.PipelinePositioningService.displayPipeline(this.rawPipelineModel, "#assembly", false, autoLayout);
+        this.TransitionService.makePipelineAssemblyEmpty(false);
+        this.pipelineValid = this.PipelineValidationService.isValidPipeline(this.rawPipelineModel);
+    }
+
     toggleErrorMessagesDisplayed() {
         this.errorMessagesDisplayed = !(this.errorMessagesDisplayed);
     }
diff --git a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.tmpl.html b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.tmpl.html
index 5f0865f..be0d990 100644
--- a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.tmpl.html
+++ b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.tmpl.html
@@ -56,6 +56,13 @@
                     Auto Layout
                 </md-tooltip>
             </md-button>
+            <div class="pipeline-cache-block">
+                <div ng-if="ctrl.pipelineCached">All pipeline modifications saved.</div>
+                <div ng-if="ctrl.pipelineCacheRunning"><md-progress-circular
+                        md-mode="indeterminate" class="pipeline-cache-progress"
+                        md-diameter="10"></md-progress-circular>&nbsp;Saving pipeline
+                    modifications</div>
+            </div>
             <span flex></span>
             <div style="position:relative;">
             <div ng-if="!ctrl.isPipelineAssemblyEmpty()" class="pipeline-validation-summary {{ctrl.PipelineValidationService.errorMessages.length > 0 ? 'pipeline-validation-summary-error' : ''}}" ng-click="ctrl.toggleErrorMessagesDisplayed()">
@@ -92,7 +99,13 @@
     </div>
     <div id="outerAssemblyArea" class="outerAssembly sp-blue-border-nopadding">
         <div id="assembly" class="canvas">
-            <pipeline pipeline-valid="ctrl.pipelineValid" canvas-id="assembly" raw-pipeline-model="ctrl.rawPipelineModel" all-elements="ctrl.allElements" preview="false"></pipeline>
+            <pipeline pipeline-valid="ctrl.pipelineValid"
+                      canvas-id="assembly"
+                      raw-pipeline-model="ctrl.rawPipelineModel"
+                      all-elements="ctrl.allElements"
+                      preview="false"
+                      pipeline-cached="ctrl.pipelineCached"
+                      pipeline-cache-running="ctrl.pipelineCacheRunning"></pipeline>
         </div>
     </div>
 </div>
\ No newline at end of file
diff --git a/ui/src/app/editor/components/pipeline-element-options/pipeline-element-options.controller.ts b/ui/src/app/editor/components/pipeline-element-options/pipeline-element-options.controller.ts
index aa7d710..f859130 100644
--- a/ui/src/app/editor/components/pipeline-element-options/pipeline-element-options.controller.ts
+++ b/ui/src/app/editor/components/pipeline-element-options/pipeline-element-options.controller.ts
@@ -21,6 +21,7 @@ import {JsplumbBridge} from "../../../services/jsplumb-bridge.service";
 import {JsplumbService} from "../../../services/jsplumb.service";
 import {PipelineValidationService} from "../../services/pipeline-validation.service";
 import {TransitionService} from "../../../services/transition.service";
+import {RestApi} from "../../../services/rest-api.service";
 
 export class PipelineElementOptionsController {
 
@@ -42,11 +43,12 @@ export class PipelineElementOptionsController {
     TransitionService: TransitionService;
     $rootScope: any;
     $timeout: any;
+    RestApi: RestApi;
 
     pipelineValid: boolean;
 
     constructor($rootScope, ObjectProvider, PipelineElementRecommendationService, InitTooltips, JsplumbBridge,
-                EditorDialogManager, JsplumbService, TransitionService, PipelineValidationService, $timeout) {
+                EditorDialogManager, JsplumbService, TransitionService, PipelineValidationService, $timeout, RestApi) {
         this.$rootScope = $rootScope;
         this.ObjectProvider = ObjectProvider;
         this.PipelineElementRecommendationService = PipelineElementRecommendationService;
@@ -56,6 +58,7 @@ export class PipelineElementOptionsController {
         this.JsplumbService = JsplumbService;
         this.TransitionService = TransitionService;
         this.PipelineValidationService = PipelineValidationService;
+        this.RestApi = RestApi;
 
         this.recommendationsAvailable = false;
         this.possibleElements = [];
@@ -67,6 +70,8 @@ export class PipelineElementOptionsController {
 
     $onInit() {
         this.$rootScope.$on("SepaElementConfigured", (event, item) => {
+            this.pipelineElement.settings.openCustomize = false;
+            this.RestApi.updateCachedPipeline(this.rawPipelineModel);
             if (item === this.pipelineElement.payload.DOM) {
                 this.initRecs(this.pipelineElement.payload.DOM, this.rawPipelineModel);
             }
@@ -144,4 +149,4 @@ export class PipelineElementOptionsController {
 
 PipelineElementOptionsController.$inject = ['$rootScope', 'ObjectProvider', 'PipelineElementRecommendationService',
     'InitTooltips', 'JsplumbBridge', 'EditorDialogManager', 'JsplumbService',
-    'TransitionService', 'PipelineValidationService', '$timeout'];
\ No newline at end of file
+    'TransitionService', 'PipelineValidationService', '$timeout', 'RestApi'];
\ No newline at end of file
diff --git a/ui/src/app/editor/components/pipeline/pipeline.component.ts b/ui/src/app/editor/components/pipeline/pipeline.component.ts
index e18b03a..912cd43 100644
--- a/ui/src/app/editor/components/pipeline/pipeline.component.ts
+++ b/ui/src/app/editor/components/pipeline/pipeline.component.ts
@@ -27,7 +27,9 @@ export let PipelineComponent = {
         allElements: "=",
         preview: "<",
         canvasId: "@",
-        pipelineValid: "="
+        pipelineValid: "=",
+        pipelineCacheRunning: "=",
+        pipelineCached: "="
     },
     controller: PipelineController,
     controllerAs: 'ctrl'
diff --git a/ui/src/app/editor/components/pipeline/pipeline.controller.ts b/ui/src/app/editor/components/pipeline/pipeline.controller.ts
index 739d7d7..b45fb98 100644
--- a/ui/src/app/editor/components/pipeline/pipeline.controller.ts
+++ b/ui/src/app/editor/components/pipeline/pipeline.controller.ts
@@ -18,6 +18,7 @@
 
 import * as angular from "angular";
 import {PipelineValidationService} from "../../services/pipeline-validation.service";
+import {RestApi} from "../../../services/rest-api.service";
 
 export class PipelineController {
 
@@ -41,11 +42,15 @@ export class PipelineController {
     TransitionService: any;
     ShepherdService: any;
     $rootScope: any;
+    RestApi: RestApi;
+
+    pipelineCacheRunning: boolean;
+    pipelineCached: boolean;
 
     pipelineValid: boolean = false;
 
     constructor($timeout, JsplumbService, PipelineEditorService, JsplumbBridge, ObjectProvider, DialogBuilder,
-                EditorDialogManager, TransitionService, ShepherdService, $rootScope, PipelineValidationService) {
+                EditorDialogManager, TransitionService, ShepherdService, $rootScope, PipelineValidationService, RestApi) {
         this.plumbReady = false;
         this.JsplumbBridge = JsplumbBridge;
         this.JsplumbService = JsplumbService;
@@ -59,6 +64,7 @@ export class PipelineController {
         this.ShepherdService = ShepherdService;
         this.$rootScope = $rootScope;
         this.PipelineValidationService = PipelineValidationService;
+        this.RestApi = RestApi;
 
         this.currentPipelineModel = {};
         this.idCounter = 0;
@@ -169,6 +175,7 @@ export class PipelineController {
                 }
                 this.JsplumbBridge.repaintEverything();
                 this.validatePipeline();
+                this.triggerPipelineCacheUpdate();
             }
 
         }); //End #assembly.droppable()
@@ -212,6 +219,7 @@ export class PipelineController {
             this.TransitionService.makePipelineAssemblyEmpty(true);
         }
         this.JsplumbBridge.repaintEverything();
+        this.RestApi.updateCachedPipeline(this.rawPipelineModel);
     }
 
     initPlumb() {
@@ -317,8 +325,26 @@ export class PipelineController {
         return custom;
     }
 
+    triggerPipelineCacheUpdate() {
+        this.pipelineCacheRunning = true;
+        this.RestApi.updateCachedPipeline(this.rawPipelineModel).then(msg => {
+           this.pipelineCacheRunning = false;
+           this.pipelineCached = true;
+        });
+    }
+
 
 }
 
-PipelineController.$inject = ['$timeout', 'JsplumbService', 'PipelineEditorService', 'JsplumbBridge', 'ObjectProvider',
-    'DialogBuilder', 'EditorDialogManager', 'TransitionService', 'ShepherdService', '$rootScope', 'PipelineValidationService']
\ No newline at end of file
+PipelineController.$inject = ['$timeout',
+    'JsplumbService',
+    'PipelineEditorService',
+    'JsplumbBridge',
+    'ObjectProvider',
+    'DialogBuilder',
+    'EditorDialogManager',
+    'TransitionService',
+    'ShepherdService',
+    '$rootScope',
+    'PipelineValidationService',
+    'RestApi'];
\ No newline at end of file
diff --git a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.controller.ts b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.controller.ts
index 40d5868..b18ca72 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.controller.ts
+++ b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.controller.ts
@@ -122,6 +122,7 @@ export class SavePipelineController {
         this.displaySuccess(data);
         this.hide();
         this.TransitionService.makePipelineAssemblyEmpty(true);
+        this.RestApi.removePipelineFromCache();
         if (this.ShepherdService.isTourActive()) {
             this.ShepherdService.hideCurrentStep();
         }
diff --git a/ui/src/app/pipeline-details/components/preview/pipeline-preview.controller.ts b/ui/src/app/pipeline-details/components/preview/pipeline-preview.controller.ts
index 0bf78b0..caba1c4 100644
--- a/ui/src/app/pipeline-details/components/preview/pipeline-preview.controller.ts
+++ b/ui/src/app/pipeline-details/components/preview/pipeline-preview.controller.ts
@@ -41,7 +41,7 @@ export class PipelinePreviewController {
             var elid = "#" + this.jspcanvas;
             this.rawPipelineModel = this.JsplumbService.makeRawPipeline(this.pipeline, true);
             this.$timeout(() => {
-                this.PipelinePositioningService.displayPipeline(this.rawPipelineModel, elid, true);
+                this.PipelinePositioningService.displayPipeline(this.rawPipelineModel, elid, true, true);
                 var existingEndpointIds = [];
                 this.$timeout(() => {
                     this.JsplumbBridge.selectEndpoints().each(endpoint => {
diff --git a/ui/src/app/services/pipeline-positioning.service.ts b/ui/src/app/services/pipeline-positioning.service.ts
index d2fac05..c1c2366 100644
--- a/ui/src/app/services/pipeline-positioning.service.ts
+++ b/ui/src/app/services/pipeline-positioning.service.ts
@@ -39,7 +39,7 @@ export class PipelinePositioningService {
     }
 
 
-    displayPipeline(rawPipelineModel, targetCanvas, isPreview) {
+    displayPipeline(rawPipelineModel, targetCanvas, isPreview, autoLayout) {
         var jsplumbConfig = isPreview ? this.JsplumbConfigService.getPreviewConfig() : this.JsplumbConfigService.getEditorConfig();
 
         for (var i = 0; i < rawPipelineModel.length; i++) {
@@ -56,7 +56,9 @@ export class PipelinePositioningService {
         }
 
         this.connectPipelineElements(rawPipelineModel, !isPreview, jsplumbConfig);
-        this.layoutGraph(targetCanvas, "span[id^='jsplumb']", isPreview ? 75 : 110, isPreview);
+        if (autoLayout) {
+            this.layoutGraph(targetCanvas, "span[id^='jsplumb']", isPreview ? 75 : 110, isPreview);
+        }
         this.JsplumbBridge.repaintEverything();
     }
 
@@ -92,34 +94,38 @@ export class PipelinePositioningService {
             var pe = json[i];
 
             if (pe.type == "sepa") {
-                for (var j = 0, connection; connection = pe.payload.connectedTo[j]; j++) {
-                    source = connection;
-                    target = pe.payload.DOM;
-
-                    var options;
-                    var id = "#" + source;
-                    if ($(id).hasClass("sepa")) {
-                        options = jsplumbConfig.sepaEndpointOptions;
-                    } else {
-                        options = jsplumbConfig.streamEndpointOptions;
+                if (pe.payload.connectedTo) {
+                    for (var j = 0, connection; connection = pe.payload.connectedTo[j]; j++) {
+                        source = connection;
+                        target = pe.payload.DOM;
+
+                        var options;
+                        var id = "#" + source;
+                        if ($(id).hasClass("sepa")) {
+                            options = jsplumbConfig.sepaEndpointOptions;
+                        } else {
+                            options = jsplumbConfig.streamEndpointOptions;
+                        }
+
+                        let sourceEndpointId = "out-" + connection;
+                        let targetEndpointId = "in-" + j + "-" + pe.payload.DOM;
+                        this.JsplumbBridge.connect(
+                            {uuids: [sourceEndpointId, targetEndpointId], detachable: detachable}
+                        );
                     }
-
-                    let sourceEndpointId = "out-" + connection;
-                    let targetEndpointId = "in-" + j + "-" + pe.payload.DOM;
-                    this.JsplumbBridge.connect(
-                        {uuids: [sourceEndpointId, targetEndpointId], detachable: detachable}
-                    );
                 }
             } else if (pe.type == "action") {
                 target = pe.payload.DOM;
 
-                for (var j = 0, connection; connection = pe.payload.connectedTo[j]; j++) {
-                    source = connection;
-                    let sourceEndpointId = "out-" + connection;
-                    let targetEndpointId = "in-" + j + "-" + target;
-                    this.JsplumbBridge.connect(
-                        {uuids: [sourceEndpointId, targetEndpointId], detachable: detachable}
-                    );
+                if (pe.payload.connectedTo) {
+                    for (var j = 0, connection; connection = pe.payload.connectedTo[j]; j++) {
+                        source = connection;
+                        let sourceEndpointId = "out-" + connection;
+                        let targetEndpointId = "in-" + j + "-" + target;
+                        this.JsplumbBridge.connect(
+                            {uuids: [sourceEndpointId, targetEndpointId], detachable: detachable}
+                        );
+                    }
                 }
             }
         }
diff --git a/ui/src/app/services/rest-api.service.ts b/ui/src/app/services/rest-api.service.ts
index cba2afc..122aeff 100644
--- a/ui/src/app/services/rest-api.service.ts
+++ b/ui/src/app/services/rest-api.service.ts
@@ -565,6 +565,18 @@ export class RestApi {
     getFileMetadata() {
         return this.$http.get(this.urlBase() + "/files");
     }
+
+    getCachedPipeline() {
+        return this.$http.get(this.urlBase() + "/pipeline-cache");
+    }
+
+    updateCachedPipeline(rawPipelineModel: any) {
+        return this.$http.post(this.urlBase() + "/pipeline-cache", rawPipelineModel);
+    }
+
+    removePipelineFromCache() {
+        return this.$http.delete(this.urlBase() + "/pipeline-cache");
+    }
 }
 
 //RestApi.$inject = ['$http', 'apiConstants', 'AuthStatusService'];
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 43bd784..fe268af 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -50,6 +50,7 @@
 @import './sp/mat-tab';
 @import './sp/dialog.ng5';
 @import './sp/feedback.ng1';
+@import './sp/pipeline-assembly.scss';
 
 @import './sp/input.ng1';
 @import './sp/documentation.ng1';
diff --git a/ui/src/scss/sp/pipeline-assembly.scss b/ui/src/scss/sp/pipeline-assembly.scss
new file mode 100644
index 0000000..01435ca
--- /dev/null
+++ b/ui/src/scss/sp/pipeline-assembly.scss
@@ -0,0 +1,12 @@
+.pipeline-cache-progress svg path {
+  stroke: white;
+}
+
+.pipeline-cache-progress {
+  display:inline-block;
+}
+
+.pipeline-cache-block {
+  display:inline-block;
+  margin-left:15px;
+}
\ No newline at end of file