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 2021/03/09 21:50:04 UTC

[incubator-streampipes] 02/06: [STREAMPIPES-304] Bump and migrate Jsplumb version

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

commit f24b1b4a3ad14203803b8d71c3eb80354f61a39c
Author: Dominik Riemer <ri...@fzi.de>
AuthorDate: Sun Mar 7 21:37:21 2021 +0100

    [STREAMPIPES-304] Bump and migrate Jsplumb version
---
 ui/angular.json                                    |   1 -
 ui/package.json                                    |   3 +-
 .../pipeline-assembly.component.html               |   9 +-
 .../pipeline-assembly.component.scss               |  18 +-
 .../pipeline-assembly.component.ts                 |  32 +++-
 .../pipeline-element-options.component.ts          |   8 +-
 .../components/pipeline/pipeline.component.html    |  10 +-
 .../components/pipeline/pipeline.component.ts      |  79 +++++---
 ui/src/app/editor/editor.module.ts                 |  17 +-
 .../filter/enabled-pipeline-element.filter.ts      |  15 ++
 ui/src/app/editor/model/editor.model.ts            |   7 +-
 ui/src/app/editor/model/jsplumb.model.ts           |  26 +++
 .../app/editor/services/jsplumb-bridge.service.ts  |  79 +++++---
 .../app/editor/services/jsplumb-config.service.ts  |  48 ++---
 .../editor/services/jsplumb-endpoint.service.ts    |  63 +++++++
 .../app/editor/services/jsplumb-factory.service.ts |  85 +++++++++
 ui/src/app/editor/services/jsplumb.service.ts      | 210 +++++++++------------
 .../app/editor/services/object-provider.service.ts |  24 ++-
 .../services/pipeline-canvas-scrolling.service.ts  |  35 ++++
 .../app/editor/services/pipeline-editor.service.ts |  19 +-
 .../services/pipeline-element-dragged.service.ts   |  31 +++
 .../pipeline-element-recommendation.service.ts     |   8 +-
 .../services/pipeline-positioning.service.ts       | 124 ++++++------
 .../editor/services/pipeline-validation.service.ts |  42 ++---
 .../preview/pipeline-preview.component.ts          |   8 +-
 ui/src/scss/main.scss                              |   4 +-
 ui/src/scss/sp/jsplumb.scss                        |  21 +++
 ui/src/scss/sp/main.scss                           |   8 +-
 28 files changed, 694 insertions(+), 340 deletions(-)

diff --git a/ui/angular.json b/ui/angular.json
index 42cdf7b..c1ae092 100644
--- a/ui/angular.json
+++ b/ui/angular.json
@@ -39,7 +39,6 @@
               "node_modules/datatables.net/js/jquery.dataTables.js",
               "node_modules/jquery.panzoom/dist/jquery.panzoom.js",
               "node_modules/jquery-ui-dist/jquery-ui.js",
-              "node_modules/jsplumb/dist/js/jsPlumb-2.1.3-min.js",
               "node_modules/quill/dist/quill.js",
             ]
           },
diff --git a/ui/package.json b/ui/package.json
index ad82c27..8d3a3b9 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -60,7 +60,7 @@
     "jquery-ui-dist": "1.12.1",
     "jquery.panzoom": "2.0.5",
     "jshint": "2.11.1",
-    "jsplumb": "2.1.3",
+    "jsplumb": "^2.15.5",
     "jszip": "3.2.1",
     "konva": "3.2.4",
     "leaflet": "1.6.0",
@@ -74,6 +74,7 @@
     "ngx-auto-scroll": "^1.1.0",
     "ngx-color-picker": "9.0.0",
     "ngx-echarts": "^6.0.0",
+    "ngx-perfect-scrollbar": "^10.1.0",
     "ngx-quill": "12.0.1",
     "ngx-showdown": "5.1.0",
     "path": "^0.12.7",
diff --git a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html
index f2424b9..1eddb4e 100644
--- a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html
+++ b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html
@@ -20,10 +20,9 @@
     <div class="pipeline-assembly-options sp-blue-bg">
         <div fxFlex="100" fxLayout="row" fxLayoutAlign="start center">
             <button mat-button matTooltip="Save Pipeline" [matTooltipPosition]="'above'"
-                    style="display:flex;align-items:center;"
                     [disabled]="!PipelineValidationService.pipelineValid"
                     (click)="submit()" type="submit">
-                <mat-icon>save</mat-icon>&nbsp;<span>Save pipeline</span>
+                <i class="material-icons">save</i>&nbsp;<span>Save pipeline</span>
             </button>
             <button mat-button mat-icon-button matTooltip="Pan" [matTooltipPosition]="'above'"
                     [disabled]="!selectMode"
@@ -103,7 +102,7 @@
         </div>
     </div>
     <div id="outerAssemblyArea" class="outerAssembly sp-blue-border-nopadding">
-        <div id="assembly" class="canvas">
+        <div id="assembly" class="canvas jtk-surface jtk-surface-no-pan">
             <pipeline [pipelineValid]="pipelineValid"
                       [canvasId]="'assembly'"
                       [rawPipelineModel]="rawPipelineModel"
@@ -112,8 +111,8 @@
                       [pipelineCached]="pipelineCached"
                       [pipelineCacheRunning]="pipelineCacheRunning"
                       (pipelineCachedChanged)="pipelineCached=$event"
-                      (pipelineCacheRunningChanged)="pipelineCacheRunning=$event"
-            ></pipeline>
+                      (pipelineCacheRunningChanged)="pipelineCacheRunning=$event">
+            </pipeline>
         </div>
     </div>
 </div>
diff --git a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.scss b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.scss
index d252c0d..5bbd778 100644
--- a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.scss
+++ b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.scss
@@ -30,13 +30,12 @@
 }
 
 .outerAssembly {
-    position: relative;
+    //position: relative;
     left: 0px;
     top: 0px;
     overflow: visible;
     //box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .2), 0 1px 1px 0 rgba(0, 0, 0, .14), 0 2px 1px -1px rgba(0, 0, 0, .12);
-    padding: 5px;
-    height: 100%;
+    //padding: 5px;
     flex: 1;
     width: 100%;
 }
@@ -46,12 +45,12 @@
 }
 
 .canvas {
-    overflow: visible;
-    position: absolute;
+    overflow: auto;
+    position: relative;
     left: 0px;
     top: 0px;
-    height: 300%;
-    width: 300%;
+    height: 100%;
+    width: 100%;
     z-index: 0;
     margin: -1px;
 
@@ -63,3 +62,8 @@
 .pipeline-assembly-options {
     color: white;
 }
+
+.jtk-surface-nopan {
+    overflow: scroll !important;
+    cursor:default;
+}
diff --git a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.ts b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.ts
index 6979f0e..8b44161 100644
--- a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.ts
+++ b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.ts
@@ -16,7 +16,16 @@
  *
  */
 
-import {Component, EventEmitter, Input, NgZone, OnInit, Output,} from "@angular/core";
+import {
+    Component,
+    ElementRef,
+    EventEmitter,
+    Input,
+    NgZone,
+    OnInit,
+    Output,
+    ViewChild,
+} from "@angular/core";
 import {JsplumbBridge} from "../../services/jsplumb-bridge.service";
 import {PipelinePositioningService} from "../../services/pipeline-positioning.service";
 import {PipelineValidationService} from "../../services/pipeline-validation.service";
@@ -31,6 +40,8 @@ import {ConfirmDialogComponent} from "../../../core-ui/dialog/confirm-dialog/con
 import {MatDialog} from "@angular/material/dialog";
 import {EditorService} from "../../services/editor.service";
 import {PipelineService} from "../../../platform-services/apis/pipeline.service";
+import {PipelineCanvasScrollingService} from "../../services/pipeline-canvas-scrolling.service";
+import {JsplumbFactoryService} from "../../services/jsplumb-factory.service";
 
 
 @Component({
@@ -52,6 +63,8 @@ export class PipelineAssemblyComponent implements OnInit {
     @Output()
     pipelineCanvasMaximizedEmitter: EventEmitter<boolean> = new EventEmitter<boolean>();
 
+    JsplumbBridge: JsplumbBridge;
+
     pipelineCanvasMaximized: boolean = false;
 
     currentMouseOverElement: any;
@@ -69,7 +82,10 @@ export class PipelineAssemblyComponent implements OnInit {
     pipelineCacheRunning: boolean = false;
     pipelineCached: boolean = false;
 
-    constructor(private JsplumbBridge: JsplumbBridge,
+    config: any = {};
+    @ViewChild("assembly") pipelineCanvas: ElementRef;
+
+    constructor(private JsPlumbFactoryService: JsplumbFactoryService,
                 private PipelinePositioningService: PipelinePositioningService,
                 private ObjectProvider: ObjectProvider,
                 public EditorService: EditorService,
@@ -79,7 +95,8 @@ export class PipelineAssemblyComponent implements OnInit {
                 private ShepherdService: ShepherdService,
                 private dialogService: DialogService,
                 private dialog: MatDialog,
-                private ngZone: NgZone) {
+                private ngZone: NgZone,
+                private pipelineCanvasScrollingService: PipelineCanvasScrollingService) {
 
         this.selectMode = true;
         this.currentZoomLevel = 1;
@@ -87,11 +104,18 @@ export class PipelineAssemblyComponent implements OnInit {
     }
 
     ngOnInit(): void {
+        this.JsplumbBridge = this.JsPlumbFactoryService.getJsplumbBridge(false);
         if (this.currentModifiedPipelineId) {
             this.displayPipelineById();
         } else {
             this.checkAndDisplayCachedPipeline();
         }
+        this.pipelineCanvasScrollingService.canvasScrollYSubject.subscribe(position => {
+            console.log("scrolling to" + position);
+            console.log(this.pipelineCanvas.nativeElement.scrollHeight);
+            console.log(this.pipelineCanvas.nativeElement.scrollTop);
+            this.pipelineCanvas.nativeElement.scrollTop = this.pipelineCanvas.nativeElement.scrollHeight;
+        });
     }
 
     ngAfterViewInit() {
@@ -231,7 +255,7 @@ export class PipelineAssemblyComponent implements OnInit {
             this.EditorService.makePipelineAssemblyEmpty(false);
             this.ngZone.run(() => {
                 this.pipelineValid = this.PipelineValidationService
-                    .isValidPipeline(this.rawPipelineModel.filter(pe => !(pe.settings.disabled)));
+                    .isValidPipeline(this.rawPipelineModel.filter(pe => !(pe.settings.disabled)), false);
             });
         });
     }
diff --git a/ui/src/app/editor/components/pipeline-element-options/pipeline-element-options.component.ts b/ui/src/app/editor/components/pipeline-element-options/pipeline-element-options.component.ts
index 6d20d18..4c89316 100644
--- a/ui/src/app/editor/components/pipeline-element-options/pipeline-element-options.component.ts
+++ b/ui/src/app/editor/components/pipeline-element-options/pipeline-element-options.component.ts
@@ -36,6 +36,7 @@ import {CompatibleElementsComponent} from "../../dialog/compatible-elements/comp
 import {Tuple2} from "../../../core-model/base/Tuple2";
 import { cloneDeep } from "lodash";
 import {Observable, Subscription} from "rxjs";
+import {JsplumbFactoryService} from "../../services/jsplumb-factory.service";
 
 @Component({
   selector: 'pipeline-element-options',
@@ -79,11 +80,13 @@ export class PipelineElementOptionsComponent implements OnInit, OnDestroy {
 
   pipelineElementConfiguredObservable: Subscription;
 
+  JsplumbBridge: JsplumbBridge;
+
   constructor(private ObjectProvider: ObjectProvider,
               private PipelineElementRecommendationService: PipelineElementRecommendationService,
               private DialogService: DialogService,
               private EditorService: EditorService,
-              private JsplumbBridge: JsplumbBridge,
+              private JsplumbFactoryService: JsplumbFactoryService,
               private JsplumbService: JsplumbService,
               private PipelineValidationService: PipelineValidationService,
               private RestApi: RestApi) {
@@ -91,6 +94,7 @@ export class PipelineElementOptionsComponent implements OnInit, OnDestroy {
     this.possibleElements = [];
     this.recommendedElements = [];
     this.recommendationsShown = false;
+    this.JsplumbBridge = this.JsplumbFactoryService.getJsplumbBridge(false);
   }
 
   ngOnInit() {
@@ -187,4 +191,4 @@ export class PipelineElementOptionsComponent implements OnInit, OnDestroy {
   ngOnDestroy(): void {
     this.pipelineElementConfiguredObservable.unsubscribe();
   }
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/editor/components/pipeline/pipeline.component.html b/ui/src/app/editor/components/pipeline/pipeline.component.html
index fbcd494..b3736fa 100644
--- a/ui/src/app/editor/components/pipeline/pipeline.component.html
+++ b/ui/src/app/editor/components/pipeline/pipeline.component.html
@@ -16,10 +16,12 @@
   ~
   -->
 
-<div *ngFor="let pipelineElement of rawPipelineModel" style="width:100%;height:100%;z-index:1">
-    <div *ngIf="pipelineElement.settings.disabled == undefined || !pipelineElement.settings.disabled" style="width:100%;height:100%;z-index:1">
+<div style="z-index: 1" [ngStyle]="{width: canvasWidth, height: canvasHeight}" #outerCanvas>
     <span id="{{pipelineElement.payload.dom}}" style="{{getElementCss(pipelineElement.settings)}}"
-          (click)="updateOptionsClick(pipelineElement.payload.dom)" (mouseenter)="updateMouseover(pipelineElement.payload.dom)" (mouseleave)="updateMouseover('')">
+          (click)="updateOptionsClick(pipelineElement.payload.dom)"
+          (mouseenter)="updateMouseover(pipelineElement.payload.dom)"
+          (mouseleave)="updateMouseover('')"
+          *ngFor="let pipelineElement of rawPipelineModel | enabledPipelineElement">
         <span style="z-index:5;"
               [ngClass]="getElementCssClasses(pipelineElement)">
             <div class="pipeline-element-progress-container sp-fade" *ngIf="pipelineElement.settings.loadingStatus">
@@ -45,4 +47,4 @@
         </pipeline-element-options>
     </span>
 </div>
-</div>
\ 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 dd83082..c5486e7 100644
--- a/ui/src/app/editor/components/pipeline/pipeline.component.ts
+++ b/ui/src/app/editor/components/pipeline/pipeline.component.ts
@@ -23,12 +23,12 @@ import {JsplumbBridge} from "../../services/jsplumb-bridge.service";
 import {ShepherdService} from "../../../services/tour/shepherd.service";
 import {
   ChangeDetectorRef,
-  Component,
+  Component, ElementRef,
   EventEmitter,
   Input,
-  NgZone,
+  NgZone, OnDestroy,
   OnInit,
-  Output
+  Output, ViewChild
 } from "@angular/core";
 import {
   InvocablePipelineElementUnion,
@@ -37,9 +37,9 @@ import {
 } from "../../model/editor.model";
 import {
   CustomOutputStrategy,
-  DataProcessorInvocation, ErrorMessage,
-  Pipeline,
-  SpDataStream
+  DataProcessorInvocation, DataSinkInvocation, ErrorMessage,
+  Pipeline, SpDataSet,
+  SpDataStream, SpDataStreamUnion
 } from "../../../core-model/gen/streampipes-model";
 import {ObjectProvider} from "../../services/object-provider.service";
 import {CustomizeComponent} from "../../dialog/customize/customize.component";
@@ -51,13 +51,17 @@ import {MatchingErrorComponent} from "../../dialog/matching-error/matching-error
 import {Tuple2} from "../../../core-model/base/Tuple2";
 import {ConfirmDialogComponent} from "../../../core-ui/dialog/confirm-dialog/confirm-dialog.component";
 import {MatDialog} from "@angular/material/dialog";
+import {Subject} from "rxjs";
+import {PipelineElementDraggedService} from "../../services/pipeline-element-dragged.service";
+import {PipelineCanvasScrollingService} from "../../services/pipeline-canvas-scrolling.service";
+import {JsplumbFactoryService} from "../../services/jsplumb-factory.service";
 
 @Component({
   selector: 'pipeline',
   templateUrl: './pipeline.component.html',
   styleUrls: ['./pipeline.component.css']
 })
-export class PipelineComponent implements OnInit {
+export class PipelineComponent implements OnInit, OnDestroy {
 
   @Input()
   pipelineValid: boolean;
@@ -88,25 +92,32 @@ export class PipelineComponent implements OnInit {
 
   availablePipelineElementCache: PipelineElementUnion[];
 
-  plumbReady: any;
+  plumbReady: boolean;
   currentMouseOverElement: string;
   currentPipelineModel: Pipeline;
   idCounter: any;
   currentZoomLevel: any;
   TransitionService: any;
 
-  // remove later
+  canvasWidth: string = "1000%";
+  canvasHeight: string = "1000%";
+
+  JsplumbBridge: JsplumbBridge;
+
+  //@ViewChild('outerCanvas') canvasRef: ElementRef;
 
   constructor(private JsplumbService: JsplumbService,
               private PipelineEditorService: PipelineEditorService,
-              private JsplumbBridge: JsplumbBridge,
+              private JsplumbFactoryService: JsplumbFactoryService,
               private ObjectProvider: ObjectProvider,
               private EditorService: EditorService,
               private ShepherdService: ShepherdService,
               private PipelineValidationService: PipelineValidationService,
               private dialogService: DialogService,
               private dialog: MatDialog,
-              private ngZone: NgZone) {
+              private ngZone: NgZone,
+              private pipelineElementDraggedService: PipelineElementDraggedService,
+              private pipelineCanvasScrollingService: PipelineCanvasScrollingService) {
     this.plumbReady = false;
     this.currentMouseOverElement = "";
     this.currentPipelineModel = new Pipeline();
@@ -116,21 +127,39 @@ export class PipelineComponent implements OnInit {
   }
 
   ngOnInit() {
+    // this.pipelineElementDraggedService.pipelineElementMovedSubject.subscribe(position => {
+    //   console.log(position);
+    //   console.log(this.canvasHeight);
+    //   console.log(this.canvasRef.nativeElement.offsetHeight);
+    //   console.log(this.canvasRef.nativeElement.scrollHeight);
+    //   if ((position.y + 200) > this.canvasRef.nativeElement.offsetHeight) {
+    //     this.canvasHeight = this.canvasRef.nativeElement.offsetHeight +10 + "px";
+    //     this.pipelineCanvasScrollingService.canvasScrollYSubject.next(position.y);
+    //   }
+    //   if ((position.x + 200) > this.canvasRef.nativeElement.offsetWidth) {
+    //     this.canvasWidth = this.canvasRef.nativeElement.offsetWidth + 100 + "px";
+    //   }
+    // });
+    this.JsplumbBridge = this.JsplumbFactoryService.getJsplumbBridge(this.preview);
     this.JsplumbBridge.setContainer(this.canvasId);
     this.initAssembly();
     this.initPlumb();
   }
 
+  ngAfterViewInit() {
+  }
+
   validatePipeline() {
     this.ngZone.run(() => {
       this.pipelineValid = this.PipelineValidationService
-          .isValidPipeline(this.rawPipelineModel.filter(pe => !(pe.settings.disabled)));
+          .isValidPipeline(this.rawPipelineModel.filter(pe => !(pe.settings.disabled)), this.preview);
     });
   }
 
   ngOnDestroy() {
     this.JsplumbBridge.deleteEveryEndpoint();
     this.plumbReady = false;
+    //this.pipelineElementDraggedService.pipelineElementMovedSubject.unsubscribe();
   }
 
   updateMouseover(elementId) {
@@ -203,18 +232,22 @@ export class PipelineComponent implements OnInit {
             this.rawPipelineModel.push(pipelineElementConfig);
             if (ui.draggable.hasClass('set')) {
               setTimeout(() => {
-                this.JsplumbService.setDropped(pipelineElementConfig.payload.dom, pipelineElementConfig.payload, true, false);
+                this.EditorService.updateDataSet(pipelineElementConfig.payload).subscribe(data => {
+                  (pipelineElementConfig.payload as SpDataSet).eventGrounding = data.eventGrounding;
+                  (pipelineElementConfig.payload as SpDataSet).datasetInvocationId = data.invocationId;
+                  this.JsplumbService.dataStreamDropped(pipelineElementConfig.payload.dom, pipelineElementConfig.payload as SpDataSet, true, false);
+                });
               }, 0);
             }
             else if (ui.draggable.hasClass('stream')) {
               this.checkTopicModel(pipelineElementConfig);
             } else if (ui.draggable.hasClass('sepa')) {
                 setTimeout(() => {
-                  this.JsplumbService.sepaDropped(pipelineElementConfig.payload.dom, pipelineElementConfig.payload, true, false);
+                  this.JsplumbService.dataProcessorDropped(pipelineElementConfig.payload.dom, pipelineElementConfig.payload as DataProcessorInvocation, true, false);
                 }, 10);
             } else if (ui.draggable.hasClass('action')) {
                 setTimeout(() => {
-                  this.JsplumbService.actionDropped(pipelineElementConfig.payload.dom, pipelineElementConfig.payload, true, false);
+                  this.JsplumbService.dataSinkDropped(pipelineElementConfig.payload.dom, pipelineElementConfig.payload as DataSinkInvocation, true, false);
                 }, 10);
             }
             if (this.ShepherdService.isTourActive()) {
@@ -232,7 +265,10 @@ export class PipelineComponent implements OnInit {
 
   checkTopicModel(pipelineElementConfig: PipelineElementConfig) {
       setTimeout(() => {
-        this.JsplumbService.streamDropped(pipelineElementConfig.payload.dom, pipelineElementConfig.payload, true, false);
+        this.JsplumbService.dataStreamDropped(pipelineElementConfig.payload.dom,
+            pipelineElementConfig.payload as SpDataStream,
+            true,
+            false);
       }, 10);
 
     var streamDescription = pipelineElementConfig.payload as SpDataStream;
@@ -260,7 +296,7 @@ export class PipelineComponent implements OnInit {
 
   initPlumb() {
 
-    this.JsplumbService.prepareJsplumb();
+    //this.JsplumbService.prepareJsplumb();
 
     this.JsplumbBridge.unbind("connection");
 
@@ -306,8 +342,7 @@ export class PipelineComponent implements OnInit {
               info.targetEndpoint.setType("token");
               this.validatePipeline();
               this.modifyPipeline(pipelineModificationMessage.pipelineModifications);
-              var sourceEndpoint = this.JsplumbBridge.selectEndpoints({element: info.targetEndpoint.elementId});
-              if (this.PipelineEditorService.isFullyConnected(pe)) {
+              if (this.JsplumbService.isFullyConnected(pe, this.preview)) {
                 let payload = pe.payload as InvocablePipelineElementUnion;
                 if ((payload.staticProperties && payload.staticProperties.length > 0) || this.isCustomOutput(pe)) {
                   this.showCustomizeDialog({a: false, b: pe});
@@ -411,7 +446,9 @@ export class PipelineComponent implements OnInit {
       if (c) {
         pipelineElementInfo.b.settings.openCustomize = false;
         (pipelineElementInfo.b.payload as InvocablePipelineElementUnion).configured = true;
-        this.JsplumbService.activateEndpoint(pipelineElementInfo.b.payload.dom, pipelineElementInfo.b.settings.completed);
+        if (!(pipelineElementInfo.b.payload instanceof DataSinkInvocation)) {
+          this.JsplumbBridge.activateEndpoint("out-" + pipelineElementInfo.b.payload.dom, pipelineElementInfo.b.settings.completed);
+        }
         this.JsplumbBridge.getSourceEndpoint(pipelineElementInfo.b.payload.dom).setType("token");
         this.triggerPipelineCacheUpdate();
         this.announceConfiguredElement(pipelineElementInfo.b);
@@ -425,4 +462,4 @@ export class PipelineComponent implements OnInit {
   }
 
 
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/editor/editor.module.ts b/ui/src/app/editor/editor.module.ts
index 511678d..1794b56 100644
--- a/ui/src/app/editor/editor.module.ts
+++ b/ui/src/app/editor/editor.module.ts
@@ -60,6 +60,12 @@ import {PropertySelectionComponent} from "./components/output-strategy/property-
 import {UserDefinedOutputStrategyComponent} from "./components/output-strategy/user-defined-output/user-defined-output.component";
 import {ConnectModule} from "../connect/connect.module";
 import {PipelineElementTemplateConfigComponent} from "./components/pipeline-element-template-config/pipeline-element-template-config.component";
+import {EnabledPipelineElementFilter} from "./filter/enabled-pipeline-element.filter";
+import {PipelineElementDraggedService} from "./services/pipeline-element-dragged.service";
+import {PipelineCanvasScrollingService} from "./services/pipeline-canvas-scrolling.service";
+import {PerfectScrollbarModule} from "ngx-perfect-scrollbar";
+import {JsplumbEndpointService} from "./services/jsplumb-endpoint.service";
+import {JsplumbFactoryService} from "./services/jsplumb-factory.service";
 
 @NgModule({
     imports: [
@@ -76,13 +82,15 @@ import {PipelineElementTemplateConfigComponent} from "./components/pipeline-elem
         FormsModule,
         MatProgressSpinnerModule,
         ShowdownModule,
-        ReactiveFormsModule
+        ReactiveFormsModule,
+        PerfectScrollbarModule
     ],
     declarations: [
         CompatibleElementsComponent,
         CustomizeComponent,
         CustomOutputStrategyComponent,
         EditorComponent,
+        EnabledPipelineElementFilter,
         HelpComponent,
         MatchingErrorComponent,
         MissingElementsForTutorialComponent,
@@ -104,16 +112,19 @@ import {PipelineElementTemplateConfigComponent} from "./components/pipeline-elem
     providers: [
         EditorService,
         SemanticTypeUtilsService,
-        JsplumbBridge,
+        JsplumbFactoryService,
+        JsplumbEndpointService,
         JsplumbService,
         JsplumbConfigService,
         ObjectProvider,
+        PipelineCanvasScrollingService,
+        PipelineElementDraggedService,
         PipelineEditorService,
         PipelinePositioningService,
         PipelineValidationService,
         PipelineElementRecommendationService,
         ImageChecker,
-        SafeCss
+        SafeCss,
     ],
   exports: [
     EditorComponent,
diff --git a/ui/src/app/editor/filter/enabled-pipeline-element.filter.ts b/ui/src/app/editor/filter/enabled-pipeline-element.filter.ts
new file mode 100644
index 0000000..8abb26d
--- /dev/null
+++ b/ui/src/app/editor/filter/enabled-pipeline-element.filter.ts
@@ -0,0 +1,15 @@
+import {Pipe, PipeTransform} from "@angular/core";
+import {PipelineElementConfig} from "../model/editor.model";
+
+@Pipe({
+  name: 'enabledPipelineElement',
+  pure: false
+})
+export class EnabledPipelineElementFilter implements PipeTransform {
+  transform(items: PipelineElementConfig[]): any {
+    if (!items) {
+      return items;
+    }
+    return items.filter(item => item.settings.disabled == undefined || !item.settings.disabled);
+  }
+}
diff --git a/ui/src/app/editor/model/editor.model.ts b/ui/src/app/editor/model/editor.model.ts
index 692d7c3..ab103db 100644
--- a/ui/src/app/editor/model/editor.model.ts
+++ b/ui/src/app/editor/model/editor.model.ts
@@ -29,6 +29,11 @@ export type PipelineElementHolder = {
   [key: string]: Array<PipelineElementUnion>;
 };
 
+export interface PipelineElementPosition {
+  x: number;
+  y: number;
+}
+
 export interface PipelineElementConfig {
   type: string,
   settings: {
@@ -80,4 +85,4 @@ export const PIPELINE_ELEMENT_TOKEN = new InjectionToken<{}>('pipelineElement');
 export type PipelineElementIdentifier = "org.apache.streampipes.model.SpDataStream"
     | "org.apache.streampipes.model.SpDataSet"
     | "org.apache.streampipes.model.graph.DataProcessorInvocation"
-    | "org.apache.streampipes.model.graph.DataSinkInvocation";
\ No newline at end of file
+    | "org.apache.streampipes.model.graph.DataSinkInvocation";
diff --git a/ui/src/app/editor/model/jsplumb.model.ts b/ui/src/app/editor/model/jsplumb.model.ts
new file mode 100644
index 0000000..3d9dddc
--- /dev/null
+++ b/ui/src/app/editor/model/jsplumb.model.ts
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ *
+ */
+
+export interface JsplumbSettings {
+  dotRadius: number,
+  lineWidth: number,
+  arrowWidth: number,
+  arrowLength: number,
+  arrowLineWidth: number,
+  curviness: number
+}
diff --git a/ui/src/app/editor/services/jsplumb-bridge.service.ts b/ui/src/app/editor/services/jsplumb-bridge.service.ts
index 82631d1..ca94753 100644
--- a/ui/src/app/editor/services/jsplumb-bridge.service.ts
+++ b/ui/src/app/editor/services/jsplumb-bridge.service.ts
@@ -16,107 +16,130 @@
  *
  */
 
-import {Injectable} from "@angular/core";
+import {jsPlumbInstance} from 'jsplumb'
 
-declare const jsPlumb: any;
-
-@Injectable()
 export class JsplumbBridge {
 
-    constructor() {
+    constructor(private jsPlumbInstance: jsPlumbInstance) {
+    }
+
+    activateEndpoint(endpointId: string, endpointEnabled: boolean) {
+        let endpoint = this.getEndpointById(endpointId);
+        endpoint.setEnabled(endpointEnabled);
+    }
+
+    activateEndpointWithType(endpointId: string, endpointEnabled: boolean, endpointType: string) {
+        this.activateEndpoint(endpointId, endpointEnabled);
+        this.setEndpointType(endpointId, endpointType);
+    }
+
+    setEndpointType(endpointId: string, endpointType: string) {
+        let endpoint = this.getEndpointById(endpointId);
+        // @ts-ignore
+        endpoint.setType(endpointType);
+    }
+
+    getEndpointById(endpointId: string) {
+        return this.jsPlumbInstance.getEndpoint(endpointId);
     }
 
     setZoom(scale) {
-        jsPlumb.setZoom(scale);
+        this.jsPlumbInstance.setZoom(scale);
     }
 
     repaintEverything() {
-        jsPlumb.repaintEverything(true);
+        this.jsPlumbInstance.repaintEverything(true);
     }
 
     deleteEveryEndpoint() {
-        jsPlumb.deleteEveryEndpoint();
+        this.jsPlumbInstance.deleteEveryEndpoint();
     }
 
     setContainer(container) {
-        jsPlumb.setContainer(container);
+        this.jsPlumbInstance.setContainer(container);
     }
 
     unbind(element) {
-        jsPlumb.unbind(element);
+        this.jsPlumbInstance.unbind(element);
     }
 
     bind(event, fn) {
-        return jsPlumb.bind(event, fn);
+        return this.jsPlumbInstance.bind(event, fn);
     }
 
     // TODO: Overloading Functions?
     selectEndpoints(endpoint?) {
         if (endpoint === undefined) {
-            return jsPlumb.selectEndpoints();
+            // @ts-ignore
+            return this.jsPlumbInstance.selectEndpoints();
         }
-        return jsPlumb.selectEndpoints(endpoint);
+        // @ts-ignore
+        return this.jsPlumbInstance.selectEndpoints(endpoint);
     }
 
     selectEndpointsById(id) {
-        return jsPlumb.selectEndpoints({source: id});
+        // @ts-ignore
+        return this.jsPlumbInstance.selectEndpoints({source: id});
     }
 
     getSourceEndpoint(id) {
-        return jsPlumb.selectEndpoints({source: id});
+        // @ts-ignore
+        return this.jsPlumbInstance.selectEndpoints({source: id});
     }
 
     getTargetEndpoint(id) {
-        return jsPlumb.selectEndpoints({target: id});
+        // @ts-ignore
+        return this.jsPlumbInstance.selectEndpoints({target: id});
     }
 
     getEndpointCount(id) {
-        return jsPlumb.selectEndpoints({element: id}).length;
+        // @ts-ignore
+        return this.jsPlumbInstance.selectEndpoints({element: id}).length;
     }
 
     detach(connection) {
-        jsPlumb.detach(connection);
+        this.jsPlumbInstance.deleteConnection(connection);
     }
 
     getConnections(filter) {
-        return jsPlumb.getConnections(filter);
+        return this.jsPlumbInstance.getConnections(filter, {});
     }
 
     addEndpoint(element, options) {
-        return jsPlumb.addEndpoint(element, options);
+        return this.jsPlumbInstance.addEndpoint(element, options);
     }
 
     connect(connection) {
-        jsPlumb.connect(connection);
+        this.jsPlumbInstance.connect(connection);
     }
 
     removeAllEndpoints(element) {
-        jsPlumb.removeAllEndpoints(element);
+        this.jsPlumbInstance.removeAllEndpoints(element);
     }
 
     registerEndpointTypes(typeInfo) {
-        jsPlumb.registerEndpointTypes(typeInfo);
+        this.jsPlumbInstance.registerEndpointTypes(typeInfo);
     }
 
     draggable(element, option) {
-        jsPlumb.draggable(element, option);
+        this.jsPlumbInstance.draggable(element, option);
     }
 
     // TODO: Overloading Functions?
     setSuspendDrawing(bool1, bool2?) {
         if (bool2 === undefined) {
-            jsPlumb.setSuspendDrawing(bool1);
+            this.jsPlumbInstance.setSuspendDrawing(bool1);
         } else {
-            jsPlumb.setSuspendDrawing(bool1, bool2);
+            this.jsPlumbInstance.setSuspendDrawing(bool1, bool2);
         }
     }
 
     getAllConnections() {
-        return jsPlumb.getAllConnections();
+        return this.jsPlumbInstance.getAllConnections();
     }
 
     reset() {
-        jsPlumb.reset();
+        this.jsPlumbInstance.reset();
     }
 }
 
diff --git a/ui/src/app/editor/services/jsplumb-config.service.ts b/ui/src/app/editor/services/jsplumb-config.service.ts
index c18d55b..44de61c 100644
--- a/ui/src/app/editor/services/jsplumb-config.service.ts
+++ b/ui/src/app/editor/services/jsplumb-config.service.ts
@@ -17,6 +17,7 @@
  */
 
 import {Injectable} from "@angular/core";
+import {JsplumbSettings} from "../model/jsplumb.model";
 
 @Injectable()
 export class JsplumbConfigService {
@@ -33,28 +34,33 @@ export class JsplumbConfigService {
     }
 
     makeConfig(settings) {
-        let config = {};
-        config['streamEndpointOptions'] = this.makeStreamEndpointOptions(settings);
-        config['sepaEndpointOptions'] = this.makeSepaEndpointOptions(settings);
-        config['leftTargetPointOptions'] = this.makeLeftTargetPointOptions(settings);
+        let config = {} as any;
+        config.streamEndpointOptions = this.makeStreamEndpointOptions(settings);
+        config.sepaEndpointOptions = this.makeSepaEndpointOptions(settings);
+        config.leftTargetPointOptions = this.makeLeftTargetPointOptions(settings);
         return config;
     }
 
-    makeSettings(dotRadius, lineWidth, arrowWidth, arrowLength, arrowLineWidth, curviness) {
-        let settings = {};
-        settings['dotRadius'] = dotRadius;
-        settings['lineWidth'] = lineWidth;
-        settings['arrowWidth'] = arrowWidth;
-        settings['arrowLength'] = arrowLength;
-        settings['arrowLineWidth'] = arrowLineWidth;
-        settings['curviness'] = curviness;
+    makeSettings(dotRadius: number,
+                 lineWidth: number,
+                 arrowWidth: number,
+                 arrowLength: number,
+                 arrowLineWidth: number,
+                 curviness: number) {
+        let settings = {} as JsplumbSettings;
+        settings.dotRadius = dotRadius;
+        settings.lineWidth = lineWidth;
+        settings.arrowWidth = arrowWidth;
+        settings.arrowLength = arrowLength;
+        settings.arrowLineWidth = arrowLineWidth;
+        settings.curviness = curviness;
         return settings;
     }
 
-    makeStreamEndpointOptions(settings) {
+    makeStreamEndpointOptions(settings: JsplumbSettings) {
         return {
             endpoint: ["Dot", {radius: settings.dotRadius}],
-            connectorStyle: {strokeStyle: "#BDBDBD", outlineColor: "#9E9E9E", lineWidth: settings.lineWidth},
+            connectorStyle: {stroke: "#BDBDBD", outlineStroke: "#BDBDBD", strokeWidth: settings.lineWidth},
             connector: ["Bezier", {curviness: settings.curviness}],
             isSource: true,
             maxConnections: -1,
@@ -64,8 +70,8 @@ export class JsplumbConfigService {
                 ["Arrow", {
                     width: settings.arrowWidth, length: settings.arrowLength, location: 0.5, id: "arrow", paintStyle: {
                         fillStyle: "#BDBDBD",
-                        strokeStyle: "#9E9E9E",
-                        lineWidth: settings.arrowLineWidth
+                        stroke: "#9E9E9E",
+                        strokeWidth: settings.arrowLineWidth
                     }
                 }],
             ]
@@ -76,7 +82,7 @@ export class JsplumbConfigService {
         return {
             endpoint: ["Dot", {radius: settings.dotRadius}],
             connectorStyle: {
-                strokeStyle: "#BDBDBD", outlineColor: "#9E9E9E", lineWidth: settings.lineWidth
+                stroke: "#BDBDBD", outlineStroke: "#9E9E9E", strokeWidth: settings.lineWidth
             },
             connector: ["Bezier", {curviness: settings.curviness}],
             isSource: true,
@@ -86,9 +92,9 @@ export class JsplumbConfigService {
             connectorOverlays: [
                 ["Arrow", {
                     width: settings.arrowWidth, length: settings.arrowLength, location: 0.5, id: "arrow", paintStyle: {
-                        fillStyle: "#BDBDBD",
-                        strokeStyle: "#9E9E9E",
-                        lineWidth: settings.arrowLineWidth
+                        fill: "#BDBDBD",
+                        stroke: "#9E9E9E",
+                        strokeWidth: settings.arrowLineWidth
                     }
                 }],
             ],
@@ -108,5 +114,3 @@ export class JsplumbConfigService {
     }
 
 }
-
-//JsplumbConfigService.$inject = [];
\ No newline at end of file
diff --git a/ui/src/app/editor/services/jsplumb-endpoint.service.ts b/ui/src/app/editor/services/jsplumb-endpoint.service.ts
new file mode 100644
index 0000000..e3e7be3
--- /dev/null
+++ b/ui/src/app/editor/services/jsplumb-endpoint.service.ts
@@ -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.
+ *
+ */
+
+import {Injectable} from "@angular/core";
+import {JsplumbConfigService} from "./jsplumb-config.service";
+
+@Injectable()
+export class JsplumbEndpointService {
+
+  constructor(private jsplumbConfigService: JsplumbConfigService) {
+
+  }
+
+  getJsplumbConfig(preview): any {
+    return preview ? this.jsplumbConfigService.getPreviewConfig() : this.jsplumbConfigService.getEditorConfig();
+  }
+
+  getStreamEndpoint(preview: boolean,
+                    pipelineElementDomId: string) {
+    let jsplumbConfig = this.getJsplumbConfig(preview);
+    let config = jsplumbConfig.streamEndpointOptions;
+    config.uuid = "out-" + pipelineElementDomId;
+    return config;
+  }
+
+  getInputEndpoint(preview, pipelineElementDomId, index): any {
+    let jsplumbConfig = this.getJsplumbConfig(preview);
+    let inConfig = jsplumbConfig.leftTargetPointOptions;
+    inConfig.uuid = "in-" + index + "-" + pipelineElementDomId;
+    return inConfig;
+  }
+
+  getOutputEndpoint(preview, pipelineElementDomId): any {
+    let jsplumbConfig = this.getJsplumbConfig(preview);
+    let outConfig = jsplumbConfig.sepaEndpointOptions;
+    outConfig.uuid = "out-" + pipelineElementDomId;
+    return outConfig;
+  }
+
+  getNewTargetPoint(preview, x, y, pipelineElementDomId, index): any {
+    let inConfig = this.getInputEndpoint(preview, pipelineElementDomId, index);
+    inConfig.type = "empty";
+    inConfig.anchor = [x, y, -1, 0];
+    inConfig.isTarget = true;
+
+    return inConfig;
+  }
+}
diff --git a/ui/src/app/editor/services/jsplumb-factory.service.ts b/ui/src/app/editor/services/jsplumb-factory.service.ts
new file mode 100644
index 0000000..ff16482
--- /dev/null
+++ b/ui/src/app/editor/services/jsplumb-factory.service.ts
@@ -0,0 +1,85 @@
+/*
+ * 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 {jsPlumb, jsPlumbInstance} from "jsplumb";
+import {JsplumbBridge} from "./jsplumb-bridge.service";
+import {Injectable} from "@angular/core";
+
+@Injectable()
+export class JsplumbFactoryService {
+
+  pipelineEditorInstance: jsPlumbInstance;
+  pipelinePreviewInstance: jsPlumbInstance;
+
+  pipelineEditorBridge: JsplumbBridge;
+  pipelinePreviewBridge: JsplumbBridge;
+
+  constructor() {
+    this.pipelineEditorInstance = this.makePipelineEditorInstance();
+    this.pipelinePreviewInstance = this.makePipelinePreviewInstance();
+
+    this.pipelineEditorBridge = new JsplumbBridge(this.pipelineEditorInstance);
+    this.pipelinePreviewBridge = new JsplumbBridge(this.pipelinePreviewInstance);
+
+    this.prepareJsplumb(this.pipelineEditorInstance);
+    this.prepareJsplumb(this.pipelinePreviewInstance);
+  }
+
+  getJsplumbBridge(previewConfig: boolean): JsplumbBridge {
+    return previewConfig ? this.pipelineEditorBridge : this.pipelinePreviewBridge;
+  }
+
+  makePipelineEditorInstance(): jsPlumbInstance {
+    return jsPlumb.getInstance();
+  }
+
+  makePipelinePreviewInstance(): jsPlumbInstance {
+    return jsPlumb.getInstance();
+  }
+
+  prepareJsplumb(jsplumbInstance: jsPlumbInstance) {
+    jsplumbInstance.registerEndpointTypes({
+      "empty": {
+        paintStyle: {
+          fill: "white",
+          stroke: "#9E9E9E",
+          strokeWidth: 2,
+        }
+      },
+      "token": {
+        paintStyle: {
+          fill: "#BDBDBD",
+          stroke: "#9E9E9E",
+          strokeWidth: 2
+        },
+        hoverPaintStyle: {
+          fill: "#BDBDBD",
+          stroke: "#4CAF50",
+          strokeWidth: 4,
+        }
+      },
+      "highlight": {
+        paintStyle: {
+          fill: "white",
+          stroke: "#4CAF50",
+          strokeWidth: 4
+        }
+      }
+    });
+  }
+}
diff --git a/ui/src/app/editor/services/jsplumb.service.ts b/ui/src/app/editor/services/jsplumb.service.ts
index 96190da..4196480 100644
--- a/ui/src/app/editor/services/jsplumb.service.ts
+++ b/ui/src/app/editor/services/jsplumb.service.ts
@@ -19,63 +19,45 @@
 import {JsplumbConfigService} from "./jsplumb-config.service";
 import {JsplumbBridge} from "./jsplumb-bridge.service";
 import {Injectable} from "@angular/core";
-import {PipelineElementConfig, PipelineElementUnion} from "../model/editor.model";
+import {
+    InvocablePipelineElementUnion,
+    PipelineElementConfig,
+    PipelineElementUnion
+} from "../model/editor.model";
 import {PipelineElementTypeUtils} from "../utils/editor.utils";
 import {
     DataProcessorInvocation,
     DataSinkInvocation,
+    Pipeline,
     SpDataSet,
-    SpDataStream
+    SpDataStream,
+    SpDataStreamUnion
 } from "../../core-model/gen/streampipes-model";
-import {EditorService} from "./editor.service";
+import {PipelineElementDraggedService} from "./pipeline-element-dragged.service";
+import {JsplumbEndpointService} from "./jsplumb-endpoint.service";
+import {JsplumbFactoryService} from "./jsplumb-factory.service";
 
 @Injectable()
 export class JsplumbService {
 
-    idCounter: any;
+    idCounter: number = 0;
 
     constructor(private JsplumbConfigService: JsplumbConfigService,
-                private JsplumbBridge: JsplumbBridge,
-                private EditorService: EditorService) {
-        this.idCounter = 0;
-    }
-
-    prepareJsplumb() {
-        this.JsplumbBridge.registerEndpointTypes({
-            "empty": {
-                paintStyle: {
-                    fillStyle: "white",
-                    strokeStyle: "#9E9E9E",
-                    lineWidth: 2
-                }
-            },
-            "token": {
-                paintStyle: {
-                    fillStyle: "#BDBDBD",
-                    strokeStyle: "#9E9E9E",
-                    lineWidth: 2
-                },
-                hoverPaintStyle: {
-                    fillStyle: "#BDBDBD",
-                    strokeStyle: "#4CAF50",
-                    lineWidth: 4
-                }
-            },
-            "highlight": {
-                paintStyle: {
-                    fillStyle: "white",
-                    strokeStyle: "#4CAF50",
-                    lineWidth: 4
-                }
-            }
-        });
+                private JsplumbFactory: JsplumbFactoryService,
+                private jsplumbEndpointService: JsplumbEndpointService,
+                private pipelineElementDraggedService: PipelineElementDraggedService) {
     }
 
-    activateEndpoint(endpointId, endpointEnabled) {
-        this.JsplumbBridge.selectEndpointsById(endpointId).setEnabled(endpointEnabled);
+    isFullyConnected(pipelineElementConfig: PipelineElementConfig,
+                     previewConfig: boolean) {
+        let jsplumbBridge = this.JsplumbFactory.getJsplumbBridge(previewConfig);
+        let payload = pipelineElementConfig.payload as InvocablePipelineElementUnion;
+        return payload.inputStreams == null ||
+            jsplumbBridge.getConnections({target: $("#" +payload.dom)}).length == payload.inputStreams.length;
     }
 
-    makeRawPipeline(pipelineModel, isPreview) {
+    makeRawPipeline(pipelineModel: Pipeline,
+                    isPreview: boolean) {
         return pipelineModel
             .streams
             .map(s => this.toConfig(s, "stream", isPreview))
@@ -83,59 +65,69 @@ export class JsplumbService {
             .concat(pipelineModel.actions.map(s => this.toConfig(s, "action", isPreview)));
     }
 
-    toConfig(pe, type, isPreview) {
-        pe.type = type;
+    toConfig(pe: PipelineElementUnion,
+             type: string,
+             isPreview: boolean) {
+        (pe as any).type = type;
         return this.createNewPipelineElementConfig(pe, {x: 100, y: 100}, isPreview, true);
     }
 
-
-    createElement(pipelineModel, pipelineElement, pipelineElementDomId) {
+    createElement(pipelineModel: PipelineElementConfig[],
+                  pipelineElement: InvocablePipelineElementUnion,
+                  pipelineElementDomId: string) {
         var pipelineElementDom = $("#" + pipelineElementDomId);
         var pipelineElementConfig = this.createNewPipelineElementConfigWithFixedCoordinates(pipelineElementDom, pipelineElement, false);
         pipelineModel.push(pipelineElementConfig);
         setTimeout(() => {
-            this.createAssemblyElement(pipelineElementConfig.payload.dom, pipelineElementConfig.payload, pipelineElementDom);
+            this.createAssemblyElement(pipelineElementConfig.payload.dom,
+                pipelineElementConfig.payload as InvocablePipelineElementUnion,
+                pipelineElementDom,
+                false);
         });
     }
 
-    createAssemblyElement($newElementId, json, $parentElement) {
+    createAssemblyElement(pipelineElementDomId: string,
+                          pipelineElement: InvocablePipelineElementUnion,
+                          $parentElement,
+                          previewConfig: boolean) {
         var $target;
-        if (json.belongsTo.indexOf("sepa") > 0) { //Sepa Element
-            $target = this.sepaDropped($newElementId, json, true, false);
-            this.connectNodes($parentElement, $target);
+        if (pipelineElement.belongsTo.indexOf("sepa") > 0) { //Sepa Element
+            $target = this.dataProcessorDropped(pipelineElementDomId, pipelineElement as DataProcessorInvocation, true, false);
+            this.connectNodes($parentElement, $target, previewConfig);
         } else {
-            $target = this.actionDropped($newElementId, json, true, false);
-            this.connectNodes($parentElement, $target);
+            $target = this.dataSinkDropped(pipelineElementDomId, pipelineElement, true, false);
+            this.connectNodes($parentElement, $target, previewConfig);
         }
     }
 
-    connectNodes($parentElement, $target) {
+    connectNodes($parentElement, $target, previewConfig: boolean) {
         var options;
+        let jsplumbBridge = this.getBridge(previewConfig);
         if ($parentElement.hasClass("stream")) {
             // TODO: getJsplumbConfig depends on isPreview. Not implemented yet
-            options = this.getJsplumbConfig(true).streamEndpointOptions;
+            options = this.jsplumbEndpointService.getJsplumbConfig(true).streamEndpointOptions;
         } else {
             // TODO: getJsplumbConfig depends on isPreview. Not implemented yet
-            options = this.getJsplumbConfig(true).sepaEndpointOptions;
+            options = this.jsplumbEndpointService.getJsplumbConfig(true).sepaEndpointOptions;
         }
         var sourceEndPoint;
-        if (this.JsplumbBridge.selectEndpoints({source: $parentElement}).length > 0) {
-            if (!(this.JsplumbBridge.selectEndpoints({source: $parentElement}).get(0).isFull())) {
-                sourceEndPoint = this.JsplumbBridge.selectEndpoints({source: $parentElement}).get(0)
+        if (jsplumbBridge.selectEndpoints({source: $parentElement}).length > 0) {
+            if (!(jsplumbBridge.selectEndpoints({source: $parentElement}).get(0).isFull())) {
+                sourceEndPoint = jsplumbBridge.selectEndpoints({source: $parentElement}).get(0)
             } else {
-                sourceEndPoint = this.JsplumbBridge.addEndpoint($parentElement, options);
+                sourceEndPoint = jsplumbBridge.addEndpoint($parentElement, options);
             }
         } else {
-            sourceEndPoint = this.JsplumbBridge.addEndpoint($parentElement, options);
+            sourceEndPoint = jsplumbBridge.addEndpoint($parentElement, options);
         }
 
-        var targetEndPoint = this.JsplumbBridge.selectEndpoints({target: $target}).get(0);
+        var targetEndPoint = jsplumbBridge.selectEndpoints({target: $target}).get(0);
 
-        this.JsplumbBridge.connect({source: sourceEndPoint, target: targetEndPoint, detachable: true});
-        this.JsplumbBridge.repaintEverything();
+        jsplumbBridge.connect({source: sourceEndPoint, target: targetEndPoint, detachable: true});
+        jsplumbBridge.repaintEverything();
     }
 
-    createNewPipelineElementConfigWithFixedCoordinates($parentElement, json, isPreview) {
+    createNewPipelineElementConfigWithFixedCoordinates($parentElement, json, isPreview): PipelineElementConfig {
         var x = $parentElement.position().left;
         var y = $parentElement.position().top;
         var coord = {'x': x + 200, 'y': y};
@@ -183,7 +175,7 @@ export class JsplumbService {
         }
     }
 
-    makeId(count) {
+    makeId(count: number) {
         var text = "";
         var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
 
@@ -193,82 +185,64 @@ export class JsplumbService {
         return text;
     }
 
-    streamDropped(pipelineElementDomId, json, endpoints, preview) {
+    dataStreamDropped(pipelineElementDomId: string,
+                  pipelineElement: SpDataStreamUnion,
+                  endpoints: boolean,
+                  preview: boolean) {
+        let jsplumbBridge = this.getBridge(preview);
         if (endpoints) {
             if (!preview) {
-                this.JsplumbBridge.draggable(pipelineElementDomId, {containment: 'parent'});
+                jsplumbBridge.draggable(pipelineElementDomId, {containment: 'parent',
+                    // drag: (e => {
+                    // this.pipelineElementDraggedService.notify({x: e.pos[0], y: e.pos[1]});
+                    // })
+                });
             }
 
-            this.JsplumbBridge.addEndpoint(pipelineElementDomId, this.getStreamEndpoint(preview, pipelineElementDomId));
+            let endpointOptions = this.jsplumbEndpointService.getStreamEndpoint(preview, pipelineElementDomId);
+            jsplumbBridge.addEndpoint(pipelineElementDomId, endpointOptions);
         }
         return pipelineElementDomId;
     };
 
-    setDropped($newElement, json, endpoints, preview) {
-        this.EditorService.updateDataSet(json).subscribe(data => {
-            json.eventGrounding = data.eventGrounding;
-            json.datasetInvocationId = data.invocationId;
-            this.streamDropped($newElement, json, endpoints, preview);
-        });
-    }
-
-    sepaDropped(pipelineElementDomId, json, endpoints, preview) {
-        this.actionDropped(pipelineElementDomId, json, endpoints, preview);
+    dataProcessorDropped(pipelineElementDomId: string,
+                pipelineElement: DataProcessorInvocation,
+                endpoints: boolean,
+                preview: boolean): string {
+        let jsplumbBridge = this.getBridge(preview);
+        this.dataSinkDropped(pipelineElementDomId, pipelineElement, endpoints, preview);
         if (endpoints) {
-            this.JsplumbBridge.addEndpoint(pipelineElementDomId, this.getOutputEndpoint(preview, pipelineElementDomId));
+            jsplumbBridge.addEndpoint(pipelineElementDomId,
+                this.jsplumbEndpointService.getOutputEndpoint(preview, pipelineElementDomId));
         }
         return pipelineElementDomId;
     };
 
-    actionDropped(pipelineElementDomId, json, endpoints, preview) {
+    dataSinkDropped(pipelineElementDomId: string,
+                  pipelineElement: InvocablePipelineElementUnion,
+                  endpoints: boolean,
+                  preview: boolean): string {
+        let jsplumbBridge = this.getBridge(preview);
         if (!preview) {
-            this.JsplumbBridge.draggable(pipelineElementDomId, {containment: 'parent'});
+            jsplumbBridge.draggable(pipelineElementDomId, {containment: 'parent'});
         }
 
         if (endpoints) {
-            if (json.inputStreams.length < 2) { //1 InputNode
-                this.JsplumbBridge.addEndpoint(pipelineElementDomId, this.getInputEndpoint(preview, pipelineElementDomId, 0));
+            if (pipelineElement.inputStreams.length < 2) { //1 InputNode
+                jsplumbBridge.addEndpoint(pipelineElementDomId,
+                    this.jsplumbEndpointService.getInputEndpoint(preview, pipelineElementDomId, 0));
             } else {
-                this.JsplumbBridge.addEndpoint(pipelineElementDomId, this.getNewTargetPoint(preview, 0, 0.25, pipelineElementDomId, 0));
-                this.JsplumbBridge.addEndpoint(pipelineElementDomId, this.getNewTargetPoint(preview, 0, 0.75, pipelineElementDomId, 1));
+                jsplumbBridge.addEndpoint(pipelineElementDomId,
+                    this.jsplumbEndpointService.getNewTargetPoint(preview, 0, 0.25, pipelineElementDomId, 0));
+                jsplumbBridge.addEndpoint(pipelineElementDomId,
+                    this.jsplumbEndpointService.getNewTargetPoint(preview, 0, 0.75, pipelineElementDomId, 1));
             }
         }
         return pipelineElementDomId;
     };
 
-    getJsplumbConfig(preview): any {
-        return preview ? this.JsplumbConfigService.getPreviewConfig() : this.JsplumbConfigService.getEditorConfig();
+    getBridge(previewConfig: boolean): JsplumbBridge {
+        return this.JsplumbFactory.getJsplumbBridge(previewConfig);
     }
 
-    getStreamEndpoint(preview, pipelineElementDomId) {
-        var jsplumbConfig = this.getJsplumbConfig(preview);
-        let config = jsplumbConfig.streamEndpointOptions;
-        config.uuid = "out-" + pipelineElementDomId;
-        return config;
-    }
-
-    getInputEndpoint(preview, pipelineElementDomId, index): any {
-        var jsplumbConfig = this.getJsplumbConfig(preview);
-        let inConfig = jsplumbConfig.leftTargetPointOptions;
-        inConfig.uuid = "in-" + index + "-" + pipelineElementDomId;
-        return inConfig;
-    }
-
-    getOutputEndpoint(preview, pipelineElementDomId): any {
-        var jsplumbConfig = this.getJsplumbConfig(preview);
-        let outConfig = jsplumbConfig.sepaEndpointOptions;
-        outConfig.uuid = "out-" + pipelineElementDomId;
-        return outConfig;
-    }
-
-    getNewTargetPoint(preview, x, y, pipelineElementDomId, index): any {
-        let inConfig = this.getInputEndpoint(preview, pipelineElementDomId, index);
-        inConfig.type = "empty";
-        inConfig.anchor = [x, y, -1, 0];
-        inConfig.isTarget = true;
-
-        return inConfig;
-    }
 }
-
-//JsplumbService.$inject = ['JsplumbConfigService', 'JsplumbBridge', '$timeout', 'RestApi'];
diff --git a/ui/src/app/editor/services/object-provider.service.ts b/ui/src/app/editor/services/object-provider.service.ts
index 1562e55..5e951c7 100644
--- a/ui/src/app/editor/services/object-provider.service.ts
+++ b/ui/src/app/editor/services/object-provider.service.ts
@@ -18,17 +18,17 @@
 
 import {Injectable} from "@angular/core";
 import {RestApi} from "../../services/rest-api.service";
-import {JsplumbBridge} from "./jsplumb-bridge.service";
 import {InvocablePipelineElementUnion, PipelineElementConfig} from "../model/editor.model";
-import {Pipeline} from "../../core-model/gen/streampipes-model";
+import {DataSinkInvocation, Pipeline} from "../../core-model/gen/streampipes-model";
 import {EditorService} from "./editor.service";
+import {JsplumbFactoryService} from "./jsplumb-factory.service";
 
 @Injectable()
 export class ObjectProvider {
 
     constructor(private RestApi: RestApi,
-                private JsplumbBridge: JsplumbBridge,
-                private EditorService: EditorService) {
+                private EditorService: EditorService,
+                private JsplumbFactoryService: JsplumbFactoryService) {
     }
 
     prepareElement(pipelineElement: InvocablePipelineElementUnion) {
@@ -57,6 +57,19 @@ export class ObjectProvider {
         return pipeline;
     }
 
+    hasConnectedPipelineElement(pipelineElementDomId: string,
+                                rawPipelineModel: PipelineElementConfig[]) {
+        let pipelineElement = this.findElement(pipelineElementDomId, rawPipelineModel);
+        if (pipelineElement.payload instanceof DataSinkInvocation) {
+            return false;
+        } else {
+            return rawPipelineModel
+                .filter(pe => !pe.settings.disabled && pe.payload.connectedTo)
+                .find(pe => (pe.payload.connectedTo.indexOf(pipelineElementDomId) > -1))
+                != undefined;
+        }
+    }
+
     findElement(elementId, rawPipelineModel: PipelineElementConfig[]): PipelineElementConfig {
         let result = {} as PipelineElementConfig;
         rawPipelineModel.forEach(pe => {
@@ -68,12 +81,13 @@ export class ObjectProvider {
     }
 
     addElementNew(pipeline, currentPipelineElements: PipelineElementConfig[]): Pipeline {
+        let JsplumbBridge = this.JsplumbFactoryService.getJsplumbBridge(false);
         currentPipelineElements.forEach(pe => {
             if (pe.settings.disabled == undefined || !(pe.settings.disabled)) {
                 if (pe.type === 'sepa' || pe.type === 'action') {
                     let payload = pe.payload;
                     payload = this.prepareElement(payload as InvocablePipelineElementUnion);
-                    let connections = this.JsplumbBridge.getConnections({
+                    let connections = JsplumbBridge.getConnections({
                         target: $("#" + payload.dom)
                     });
                     for (let i = 0; i < connections.length; i++) {
diff --git a/ui/src/app/editor/services/pipeline-canvas-scrolling.service.ts b/ui/src/app/editor/services/pipeline-canvas-scrolling.service.ts
new file mode 100644
index 0000000..4eef96e
--- /dev/null
+++ b/ui/src/app/editor/services/pipeline-canvas-scrolling.service.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 {Injectable} from "@angular/core";
+import {Subject} from "rxjs";
+
+@Injectable()
+export class PipelineCanvasScrollingService {
+
+  public canvasScrollXSubject: Subject<number> = new Subject<number>();
+  public canvasScrollYSubject: Subject<number> = new Subject<number>();
+
+  public notifyX(position: number): void {
+    this.canvasScrollXSubject.next(position);
+  }
+
+  public notifyY(position: number): void {
+    this.canvasScrollYSubject.next(position);
+  }
+}
diff --git a/ui/src/app/editor/services/pipeline-editor.service.ts b/ui/src/app/editor/services/pipeline-editor.service.ts
index c630e14..fea88eb 100644
--- a/ui/src/app/editor/services/pipeline-editor.service.ts
+++ b/ui/src/app/editor/services/pipeline-editor.service.ts
@@ -17,13 +17,11 @@
  */
 
 import {Injectable} from "@angular/core";
-import {JsplumbBridge} from "./jsplumb-bridge.service";
-import {InvocablePipelineElementUnion, PipelineElementConfig} from "../model/editor.model";
 
 @Injectable()
 export class PipelineEditorService {
 
-    constructor(private JsplumbBridge: JsplumbBridge) {
+    constructor() {
     }
 
     getCoordinates(ui, currentZoomLevel) {
@@ -36,19 +34,6 @@ export class PipelineEditorService {
         };
     }
 
-    isConnected(element) {
-        if (this.JsplumbBridge.getConnections({source: element}).length < 1 && this.JsplumbBridge.getConnections({target: element}).length < 1) {
-            return false;
-        }
-        return true;
-    }
-
-    isFullyConnected(pipelineElementConfig: PipelineElementConfig) {
-        let payload = pipelineElementConfig.payload as InvocablePipelineElementUnion;
-        return payload.inputStreams == null ||
-            this.JsplumbBridge.getConnections({target: $("#" +payload.dom)}).length == payload.inputStreams.length;
-    }
-
     getDropPositionY(helper, currentZoomLevel) {
         var newTop;
         var helperPos = helper.offset();
@@ -66,5 +51,3 @@ export class PipelineEditorService {
     }
 
 }
-
-//PipelineEditorService.$inject = ['JsplumbBridge'];
\ No newline at end of file
diff --git a/ui/src/app/editor/services/pipeline-element-dragged.service.ts b/ui/src/app/editor/services/pipeline-element-dragged.service.ts
new file mode 100644
index 0000000..e209ee4
--- /dev/null
+++ b/ui/src/app/editor/services/pipeline-element-dragged.service.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 {Injectable} from "@angular/core";
+import {Subject} from "rxjs";
+import {PipelineElementPosition} from "../model/editor.model";
+
+@Injectable()
+export class PipelineElementDraggedService {
+
+  public pipelineElementMovedSubject: Subject<PipelineElementPosition> = new Subject<PipelineElementPosition>();
+
+  public notify(position: PipelineElementPosition): void {
+    this.pipelineElementMovedSubject.next(position);
+  }
+}
diff --git a/ui/src/app/editor/services/pipeline-element-recommendation.service.ts b/ui/src/app/editor/services/pipeline-element-recommendation.service.ts
index 3696e3e..d8df30c 100644
--- a/ui/src/app/editor/services/pipeline-element-recommendation.service.ts
+++ b/ui/src/app/editor/services/pipeline-element-recommendation.service.ts
@@ -20,9 +20,9 @@ import {Injectable} from "@angular/core";
 import {EditorService} from "./editor.service";
 import {PipelineElementUnion} from "../model/editor.model";
 import {
-    InvocableStreamPipesEntity,
-    PipelineElementRecommendation,
-    SpDataStream
+  InvocableStreamPipesEntity,
+  PipelineElementRecommendation,
+  SpDataStream
 } from "../../core-model/gen/streampipes-model";
 
 @Injectable()
@@ -61,4 +61,4 @@ export class PipelineElementRecommendationService {
                 .filter(pe => (pe instanceof SpDataStream && pe.elementId === belongsTo)
                     || (pe instanceof InvocableStreamPipesEntity && pe.belongsTo === belongsTo));
     }
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/editor/services/pipeline-positioning.service.ts b/ui/src/app/editor/services/pipeline-positioning.service.ts
index 2963125..ef03614 100644
--- a/ui/src/app/editor/services/pipeline-positioning.service.ts
+++ b/ui/src/app/editor/services/pipeline-positioning.service.ts
@@ -16,120 +16,118 @@
  *
  */
 
-//import * from 'lodash';
 import * as dagre from "dagre";
 import {JsplumbBridge} from "./jsplumb-bridge.service";
 import {JsplumbConfigService} from "./jsplumb-config.service";
 import {JsplumbService} from "./jsplumb.service";
 import {Injectable} from "@angular/core";
 import {PipelineElementConfig} from "../model/editor.model";
-
-declare const jsPlumb: any;
+import {
+    DataProcessorInvocation,
+    DataSinkInvocation,
+    SpDataStream
+} from "../../core-model/gen/streampipes-model";
+import {JsplumbFactoryService} from "./jsplumb-factory.service";
+import {ObjectProvider} from "./object-provider.service";
 
 @Injectable()
 export class PipelinePositioningService {
 
-
     constructor(private JsplumbService: JsplumbService,
                 private JsplumbConfigService: JsplumbConfigService,
-                private JsplumbBridge: JsplumbBridge) {
+                private JsplumbFactoryService: JsplumbFactoryService,
+                private ObjectProvider: ObjectProvider) {
     }
 
-    displayPipeline(rawPipelineModel: PipelineElementConfig[], targetCanvas, isPreview, autoLayout) {
-        var jsplumbConfig = isPreview ? this.JsplumbConfigService.getPreviewConfig() : this.JsplumbConfigService.getEditorConfig();
-
-        for (var i = 0; i < rawPipelineModel.length; i++) {
-            var currentPe = rawPipelineModel[i];
+    displayPipeline(rawPipelineModel: PipelineElementConfig[],
+                    targetCanvas,
+                    previewConfig: boolean,
+                    autoLayout: boolean) {
+        let jsPlumbBridge = this.JsplumbFactoryService.getJsplumbBridge(previewConfig);
+        let jsplumbConfig = previewConfig ? this.JsplumbConfigService.getPreviewConfig() : this.JsplumbConfigService.getEditorConfig();
+        rawPipelineModel.forEach(currentPe => {
             if (!currentPe.settings.disabled) {
-                if (currentPe.type === "stream") {
-                    this.JsplumbService.streamDropped(currentPe.payload.dom, currentPe.payload, true, isPreview);
+                if (currentPe.type === "stream" || currentPe.type === "set") {
+                    this.JsplumbService.dataStreamDropped(currentPe.payload.dom,
+                        currentPe.payload as SpDataStream,
+                        true,
+                        previewConfig);
                 }
                 if (currentPe.type === "sepa") {
-                    this.JsplumbService.sepaDropped(currentPe.payload.dom, currentPe.payload, true, isPreview);
+                    this.JsplumbService.dataProcessorDropped(currentPe.payload.dom, currentPe.payload as DataProcessorInvocation, true, previewConfig);
                 }
                 if (currentPe.type === "action") {
-                    this.JsplumbService.actionDropped(currentPe.payload.dom, currentPe.payload, true, isPreview);
+                    this.JsplumbService.dataSinkDropped(currentPe.payload.dom, currentPe.payload as DataSinkInvocation, true, previewConfig);
                 }
             }
-        }
+        });
 
-        this.connectPipelineElements(rawPipelineModel, !isPreview, jsplumbConfig);
+        this.connectPipelineElements(rawPipelineModel, previewConfig, jsplumbConfig, jsPlumbBridge);
         if (autoLayout) {
-            this.layoutGraph(targetCanvas, "span[id^='jsplumb']", isPreview ? 75 : 110, isPreview);
+            this.layoutGraph(targetCanvas, "span[id^='jsplumb']", previewConfig ? 75 : 110, previewConfig);
         }
-        this.JsplumbBridge.repaintEverything();
+        jsPlumbBridge.repaintEverything();
     }
 
     layoutGraph(canvas, nodeIdentifier, dimension, isPreview) {
+        let jsPlumbBridge = this.JsplumbFactoryService.getJsplumbBridge(isPreview);
         var g = new dagre.graphlib.Graph();
         g.setGraph({rankdir: "LR", ranksep: isPreview ? "50" : "100"});
         g.setDefaultEdgeLabel(function () {
             return {};
         });
-        var nodes = $(canvas).find(nodeIdentifier).get();
 
-        for (var i = 0; i < nodes.length; i++) {
-            var n = nodes[i];
+        var nodes = $(canvas).find(nodeIdentifier).get();
+        nodes.forEach((n, index) => {
             g.setNode(n.id, {label: n.id, width: dimension, height: dimension});
-        }
-        var edges = this.JsplumbBridge.getAllConnections();
-        for (var i = 0; i < edges.length; i++) {
-            var c = edges[i];
-            g.setEdge(c.source.id, c.target.id);
-        }
+        });
+
+        var edges = jsPlumbBridge.getAllConnections();
+        edges.forEach(edge => {
+            g.setEdge(edge.source.id, edge.target.id);
+        });
+
         dagre.layout(g);
         g.nodes().forEach(v => {
-            $("#" + v).css("left", g.node(v).x + "px");
-            $("#" + v).css("top", g.node(v).y + "px");
+            $(`#${v}`).css("left", g.node(v).x + "px");
+            $(`#${v}`).css("top", g.node(v).y + "px");
         });
     }
 
-    connectPipelineElements(rawPipelineModel: PipelineElementConfig[], detachable, jsplumbConfig) {
+    connectPipelineElements(rawPipelineModel: PipelineElementConfig[],
+                            previewConfig: boolean,
+                            jsplumbConfig: any,
+                            jsPlumbBridge: JsplumbBridge) {
         var source, target;
-
-        this.JsplumbBridge.setSuspendDrawing(true);
+        jsPlumbBridge.setSuspendDrawing(true);
         for (var i = 0; i < rawPipelineModel.length; i++) {
             var pe = rawPipelineModel[i];
 
-            if (pe.type == "sepa") {
-                if (pe.payload.connectedTo) {
-                    for (var j = 0, connection; connection = pe.payload.connectedTo[j]; j++) {
+            if (pe.type == "sepa" || pe.type == "action") {
+                if (!(pe.settings.disabled) && pe.payload.connectedTo) {
+                    pe.payload.connectedTo.forEach((connection, index) => {
                         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 inTargetEndpointId = "in-" + index + "-" + pe.payload.dom;
+                        jsPlumbBridge.connect(
+                            {
+                                uuids: [sourceEndpointId, inTargetEndpointId],
+                                detachable: !previewConfig
+                            }
                         );
-                    }
-                }
-            } else if (pe.type == "action") {
-                target = pe.payload.dom;
+                        jsPlumbBridge.activateEndpointWithType(sourceEndpointId, true, "token");
+                        jsPlumbBridge.activateEndpointWithType(inTargetEndpointId, true, "token");
 
-                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}
-                        );
-                    }
+                        if (!(pe.payload instanceof DataSinkInvocation) && !(this.ObjectProvider.hasConnectedPipelineElement(pe.payload.dom, rawPipelineModel))) {
+                            let outTargetEndpointId = "out-" + pe.payload.dom;
+                            jsPlumbBridge.activateEndpointWithType(outTargetEndpointId, true, "token");
+                        }
+                    });
                 }
             }
         }
-        this.JsplumbBridge.setSuspendDrawing(false, true);
+        jsPlumbBridge.setSuspendDrawing(false, true);
     }
-
 }
-
-//PipelinePositioningService.$inject = ['JsplumbService', 'JsplumbConfigService', 'JsplumbBridge'];
\ No newline at end of file
diff --git a/ui/src/app/editor/services/pipeline-validation.service.ts b/ui/src/app/editor/services/pipeline-validation.service.ts
index 53f7bb3..24a3df2 100644
--- a/ui/src/app/editor/services/pipeline-validation.service.ts
+++ b/ui/src/app/editor/services/pipeline-validation.service.ts
@@ -21,6 +21,7 @@ import {JsplumbBridge} from "./jsplumb-bridge.service";
 import {Injectable} from "@angular/core";
 import {PipelineElementConfig} from "../model/editor.model";
 import {DataProcessorInvocation, DataSinkInvocation} from "../../core-model/gen/streampipes-model";
+import {JsplumbFactoryService} from "./jsplumb-factory.service";
 
 @Injectable()
 export class PipelineValidationService {
@@ -36,10 +37,11 @@ export class PipelineValidationService {
         {title: "Did you configure all elements?", content: "There's a pipeline element which is missing some configuration."},
     ];
 
-    constructor(private JsplumbBridge: JsplumbBridge) {
+    constructor(private JsplumbFactoryService: JsplumbFactoryService) {
     }
 
-    isValidPipeline(rawPipelineModel) {
+    isValidPipeline(rawPipelineModel, previewConfig: boolean) {
+        let jsplumbBridge = this.JsplumbFactoryService.getJsplumbBridge(previewConfig);
         let streamInAssembly = this.isStreamInAssembly(rawPipelineModel);
         let sepaInAssembly = this.isSepaInAssembly(rawPipelineModel);
         let actionInAssembly = this.isActionInAssembly(rawPipelineModel);
@@ -48,12 +50,12 @@ export class PipelineValidationService {
         let allElementsConfigured = true;
 
         if (streamInAssembly && (sepaInAssembly || actionInAssembly)) {
-            allElementsConnected = this.allElementsConnected(rawPipelineModel);
+            allElementsConnected = this.allElementsConnected(rawPipelineModel, jsplumbBridge);
             allElementsConfigured = this.allElementsConfigured(rawPipelineModel);
         }
 
         if (streamInAssembly && actionInAssembly && allElementsConnected) {
-            onlyOnePipelineCreated = this.onlyOnePipelineCreated(rawPipelineModel);
+            onlyOnePipelineCreated = this.onlyOnePipelineCreated(rawPipelineModel, jsplumbBridge);
         }
 
         if (!this.isEmptyPipeline(rawPipelineModel)) {
@@ -96,12 +98,12 @@ export class PipelineValidationService {
             .every(config => config.settings.completed);
     }
 
-    allElementsConnected(rawPipelineModel) {
-        let g = this.makeGraph(rawPipelineModel);
-        return g.nodes().every(node => this.isFullyConnected(g, node, rawPipelineModel));
+    allElementsConnected(rawPipelineModel, jsplumbBridge) {
+        let g = this.makeGraph(rawPipelineModel, jsplumbBridge);
+        return g.nodes().every(node => this.isFullyConnected(g, node));
     }
 
-    isFullyConnected(g, node, rawPipelineModel) {
+    isFullyConnected(g, node) {
         var nodeProperty = g.node(node);
         return g.outEdges(node).length >= nodeProperty.endpointCount;
     }
@@ -118,8 +120,8 @@ export class PipelineValidationService {
         return this.isInAssembly(rawPipelineModel, "sepa");
     }
 
-    onlyOnePipelineCreated(rawPipelineModel) {
-        let g = this.makeGraph(rawPipelineModel);
+    onlyOnePipelineCreated(rawPipelineModel, jsplumbBridge: JsplumbBridge) {
+        let g = this.makeGraph(rawPipelineModel, jsplumbBridge);
         let tarjan = dagre.graphlib.alg.tarjan(g);
 
         return tarjan.length == 1;
@@ -135,7 +137,7 @@ export class PipelineValidationService {
         return isElementInAssembly;
     }
 
-    makeGraph(rawPipelineModel: PipelineElementConfig[]) {
+    makeGraph(rawPipelineModel: PipelineElementConfig[], jsplumbBridge: JsplumbBridge) {
         var g = new dagre.graphlib.Graph();
         g.setGraph({rankdir: "LR"});
         g.setDefaultEdgeLabel(function () {
@@ -151,28 +153,20 @@ export class PipelineValidationService {
                     label: n.id,
                     type: elementOptions.type,
                     name: elementOptions.payload.name,
-                    endpointCount: this.JsplumbBridge.getEndpointCount(n.id)
+                    endpointCount: jsplumbBridge.getEndpointCount(n.id)
                 });
             }
         }
-        var edges = this.JsplumbBridge.getAllConnections();
-        for (var i = 0; i < edges.length; i++) {
+        var edges = jsplumbBridge.getAllConnections();
+        edges.forEach((edge, i) => {
             var c = edges[i];
             g.setEdge(c.source.id, c.target.id);
             g.setEdge(c.target.id, c.source.id);
-        }
+        });
         return g;
     }
 
     getElementOptions(id, rawPipelineModel: PipelineElementConfig[]) {
-        var pipelineElement;
-        rawPipelineModel.forEach(pe => {
-           if (pe.payload.dom === id) {
-               pipelineElement = pe;
-           }
-        });
-        return pipelineElement;
+        return rawPipelineModel.find(pe => pe.payload.dom === id);
     }
 }
-
-//PipelineValidationService.$inject=['ObjectProvider', 'JsplumbBridge'];
\ No newline at end of file
diff --git a/ui/src/app/pipeline-details/components/preview/pipeline-preview.component.ts b/ui/src/app/pipeline-details/components/preview/pipeline-preview.component.ts
index 1fe08db..9125e2f 100644
--- a/ui/src/app/pipeline-details/components/preview/pipeline-preview.component.ts
+++ b/ui/src/app/pipeline-details/components/preview/pipeline-preview.component.ts
@@ -21,8 +21,8 @@ import {Pipeline} from "../../../core-model/gen/streampipes-model";
 import {PipelineElementConfig, PipelineElementUnion} from "../../../editor/model/editor.model";
 import {PipelinePositioningService} from "../../../editor/services/pipeline-positioning.service";
 import {JsplumbService} from "../../../editor/services/jsplumb.service";
-import {JsplumbBridge} from "../../../editor/services/jsplumb-bridge.service";
 import {ObjectProvider} from "../../../editor/services/object-provider.service";
+import {JsplumbFactoryService} from "../../../editor/services/jsplumb-factory.service";
 
 @Component({
     selector: 'pipeline-preview',
@@ -44,7 +44,7 @@ export class PipelinePreviewComponent implements OnInit {
 
     constructor(private PipelinePositioningService: PipelinePositioningService,
                 private JsplumbService: JsplumbService,
-                private JsplumbBridge: JsplumbBridge,
+                private JsplumbFactoryService: JsplumbFactoryService,
                 private ObjectProvider: ObjectProvider) {
         this.PipelinePositioningService = PipelinePositioningService;
         this.ObjectProvider = ObjectProvider;
@@ -58,7 +58,7 @@ export class PipelinePreviewComponent implements OnInit {
                 this.PipelinePositioningService.displayPipeline(this.rawPipelineModel, elid, true, true);
                 var existingEndpointIds = [];
                 setTimeout(() => {
-                    this.JsplumbBridge.selectEndpoints().each(endpoint => {
+                    this.JsplumbFactoryService.getJsplumbBridge(true).selectEndpoints().each(endpoint => {
                         if (existingEndpointIds.indexOf(endpoint.element.id) === -1) {
                             $(endpoint.element).click(() => {
                                 let payload = this.ObjectProvider.findElement(endpoint.element.id, this.rawPipelineModel).payload;
@@ -71,4 +71,4 @@ export class PipelinePreviewComponent implements OnInit {
             });
         });
     }
-}
\ No newline at end of file
+}
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 3c96525..020b977 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -24,7 +24,7 @@
 @import '~bootstrap/dist/css/bootstrap.css';
 @import '~@fortawesome/fontawesome-free/css/all.css';
 @import '~angular-ui-tree/dist/angular-ui-tree.min.css';
-@import '~jsplumb/dist/css/jsplumb.css';
+@import '~jsplumb/css/jsplumbtoolkit-defaults.css';
 @import '~ng-prettyjson/dist/ng-prettyjson.min.css';
 @import '~prismjs/themes/prism.css';
 @import '~angular-loading-bar/build/loading-bar.min.css';
@@ -35,6 +35,7 @@
 //@import '~material-design-icons/iconfont/material-icons.css';
 @import '~quill/dist/quill.snow.css';
 @import '~swagger-ui/dist/swagger-ui.css';
+@import '~perfect-scrollbar/css/perfect-scrollbar.css';
 
 @import '~roboto-fontface/css/roboto/roboto-fontface.css';
 
@@ -60,6 +61,7 @@
 @import './sp/pipeline-assembly.scss';
 @import './sp/widgets.scss';
 @import './sp/progress-bar.scss';
+@import './sp/jsplumb';
 
 @import './sp/input.ng1';
 @import './sp/documentation.ng1';
diff --git a/ui/src/scss/sp/jsplumb.scss b/ui/src/scss/sp/jsplumb.scss
new file mode 100644
index 0000000..9d828b5
--- /dev/null
+++ b/ui/src/scss/sp/jsplumb.scss
@@ -0,0 +1,21 @@
+/*!
+ * 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.
+ *
+ */
+
+.jtk-endpoint {
+  z-index: 5;
+}
diff --git a/ui/src/scss/sp/main.scss b/ui/src/scss/sp/main.scss
index 4185bdb..3d1044e 100644
--- a/ui/src/scss/sp/main.scss
+++ b/ui/src/scss/sp/main.scss
@@ -22,6 +22,10 @@ md-progress-linear.md-accent .md-container {
   background-color: rgb(168, 168, 168);
 }
 
+.stream-endpoint {
+  background: green;
+}
+
 .gu-mirror {
   display: none; /* This fixes a bug with the dragular framework */
 }
@@ -517,10 +521,6 @@ md-select.md-default-theme .md-select-value.md-select-placeholder, md-select .md
   border: 2px solid $sp-color-accent !important;
 }
 
-.jsplumb-endpoint {
-  z-index: 5;
-}
-
 .popover {
   border: 1px solid $sp-color-accent;
   box-shadow: 0 0px 0px;