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

riemer pushed a commit to branch 3119-refactor-pipeline-editor-ui-components
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to 
refs/heads/3119-refactor-pipeline-editor-ui-components by this push:
     new 7dcb1e2d14 chore(#3119): Improve pipeline editor component structure
7dcb1e2d14 is described below

commit 7dcb1e2d14060a066f8710ca1da54a79723ea19e
Author: Dominik Riemer <[email protected]>
AuthorDate: Fri Aug 9 15:49:46 2024 +0200

    chore(#3119): Improve pipeline editor component structure
---
 ...e-assembly-drawing-area-pan-zoom.component.html |  48 +++
 ...-assembly-drawing-area-pan-zoom.component.scss} |  82 +----
 ...ine-assembly-drawing-area-pan-zoom.component.ts | 142 +++++++++
 .../pipeline-assembly-drawing-area.component.html  |  54 ++++
 .../pipeline-assembly-drawing-area.component.scss} |  36 ++-
 .../pipeline-assembly-drawing-area.component.ts    | 188 ++++++++++++
 ...assembly-options-pipeline-cache.component.html} |  35 ++-
 ...assembly-options-pipeline-cache.component.scss} |  11 +-
 ...ne-assembly-options-pipeline-cache.component.ts |  66 ++++
 .../pipeline-assembly-options.component.html       |  89 ++++++
 .../pipeline-assembly-options.component.scss}      |  10 +-
 .../pipeline-assembly-options.component.ts         | 135 ++++++++
 .../pipeline-assembly.component.html               | 194 ++----------
 .../pipeline-assembly.component.scss               | 133 +-------
 .../pipeline-assembly.component.ts                 | 339 +++------------------
 .../pipeline-element-icon-stand-row.component.html |   0
 .../pipeline-element-icon-stand-row.component.scss |   0
 .../pipeline-element-icon-stand-row.component.ts   |   6 +-
 .../pipeline-element-icon-stand.component.ts       |   2 -
 .../dropped-pipeline-element.component.html        | 103 +++++++
 .../dropped-pipeline-element.component.ts          |  79 +++++
 .../components/pipeline/pipeline.component.html    | 105 ++-----
 .../components/pipeline/pipeline.component.ts      | 180 +++--------
 ui/src/app/editor/editor.component.html            |   7 +-
 ui/src/app/editor/editor.component.ts              |  82 ++++-
 ui/src/app/editor/editor.module.ts                 |  19 +-
 .../app/editor/services/jsplumb-factory.service.ts |  44 +--
 .../services/pipeline-positioning.service.ts       |   4 -
 .../elements/pipeline-elements-row.component.ts    |   2 +-
 .../preview/pipeline-preview.component.html        |  25 +-
 .../preview/pipeline-preview.component.ts          |  74 ++---
 .../pipeline-details.component.html                |   1 -
 32 files changed, 1255 insertions(+), 1040 deletions(-)

diff --git 
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area-pan-zoom/pipeline-assembly-drawing-area-pan-zoom.component.html
 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area-pan-zoom/pipeline-assembly-drawing-area-pan-zoom.component.html
new file mode 100644
index 0000000000..a14b4cc079
--- /dev/null
+++ 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area-pan-zoom/pipeline-assembly-drawing-area-pan-zoom.component.html
@@ -0,0 +1,48 @@
+<!--
+  ~ 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.
+  ~
+  -->
+
+<div class="pan-control">
+    <div class="pan-zoom-control-buttons">
+        <div class="pan-zoom-button pan-left" (click)="panLeft()">
+            <i class="material-icons">keyboard_arrow_left</i>
+        </div>
+        <div class="pan-zoom-button pan-right" (click)="panRight()">
+            <i class="material-icons">keyboard_arrow_right</i>
+        </div>
+        <div class="pan-zoom-button pan-home" (click)="panHome()">
+            <i class="material-icons">home</i>
+        </div>
+        <div class="pan-zoom-button pan-up" (click)="panUp()">
+            <i class="material-icons">keyboard_arrow_up</i>
+        </div>
+        <div class="pan-zoom-button pan-down" (click)="panDown()">
+            <i class="material-icons">keyboard_arrow_down</i>
+        </div>
+    </div>
+</div>
+<div class="zoom-control">
+    <div class="pan-zoom-control-buttons">
+        <div class="pan-zoom-button zoom-in" (click)="zoomIn()">
+            <i class="material-icons">zoom_in</i>
+        </div>
+        <mat-divider class="zoom-divider"></mat-divider>
+        <div class="pan-zoom-button zoom-out" (click)="zoomOut()">
+            <i class="material-icons">zoom_out</i>
+        </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-drawing-area/pipeline-assembly-drawing-area-pan-zoom/pipeline-assembly-drawing-area-pan-zoom.component.scss
similarity index 56%
copy from 
ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.scss
copy to 
ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area-pan-zoom/pipeline-assembly-drawing-area-pan-zoom.component.scss
index 7d714593ef..f68dd4723f 100644
--- 
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.scss
+++ 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area-pan-zoom/pipeline-assembly-drawing-area-pan-zoom.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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.
@@ -16,65 +16,6 @@
  *
  */
 
-@import '../../../../scss/_variables.scss';
-
-.mat-spinner-color::ng-deep svg circle {
-    stroke: var(--color-accent) !important;
-}
-
-.pipeline-cache-progress {
-    display: inline-block;
-}
-
-.pipeline-cache-block {
-    height: 100%;
-    margin-left: 15px;
-}
-
-.outerAssembly {
-    left: 0;
-    top: 0;
-    display: flex;
-    flex: 1 1 0;
-    overflow: hidden;
-}
-
-.pipeline-canvas-outer {
-    position: relative;
-    width: 100%;
-    height: 100%;
-}
-
-.canvas {
-    //overflow: scroll;
-    position: relative;
-    left: 0px;
-    top: 0px;
-    height: 3000px;
-    width: 4000px;
-    z-index: 0;
-    margin: -1px;
-
-    background-color: var(--color-bg-1);
-    background-image: var(--canvas-color);
-    background-size: 15px 15px;
-}
-
-.pipeline-assembly-options {
-    padding-left: 5px;
-    border: 1px solid var(--color-bg-3);
-    //background: #f6f6f6;
-}
-
-.assembly-border {
-    border: 1px solid var(--color-bg-3);
-}
-
-.jtk-surface-nopan {
-    overflow: scroll !important;
-    cursor: default;
-}
-
 .zoom-control {
     position: absolute;
     right: 10px;
@@ -87,15 +28,6 @@
     border-radius: 10px;
 }
 
-.pipeline-validation-hint {
-    position: absolute;
-    right: 10px;
-    top: 20px;
-    z-index: 10;
-    background: var(--color-bg-1);
-    box-shadow: 0.175em 0.175em 0 0 rgba(15, 28, 63, 0.125);
-}
-
 .pan-control {
     position: absolute;
     right: 60px;
@@ -157,15 +89,3 @@
     left: 28px;
     top: 30px;
 }
-
-.assembly-options-divider {
-    width: 1px;
-    height: 70%;
-    margin-left: 10px;
-    margin-right: 10px;
-    background: var(--color-bg-3);
-}
-
-.color-warn {
-    color: var(--color-warn);
-}
diff --git 
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area-pan-zoom/pipeline-assembly-drawing-area-pan-zoom.component.ts
 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area-pan-zoom/pipeline-assembly-drawing-area-pan-zoom.component.ts
new file mode 100644
index 0000000000..6685177b79
--- /dev/null
+++ 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area-pan-zoom/pipeline-assembly-drawing-area-pan-zoom.component.ts
@@ -0,0 +1,142 @@
+/*
+ * 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 {
+    AfterViewInit,
+    Component,
+    ElementRef,
+    Input,
+    OnDestroy,
+    OnInit,
+} from '@angular/core';
+import { JsplumbBridge } from '../../../../services/jsplumb-bridge.service';
+import Panzoom, { PanzoomObject } from '@panzoom/panzoom';
+import { Subscription } from 'rxjs';
+import { PipelineElementDraggedService } from 
'../../../../services/pipeline-element-dragged.service';
+
+@Component({
+    selector: 'sp-pipeline-assembly-drawing-area-pan-zoom',
+    templateUrl: './pipeline-assembly-drawing-area-pan-zoom.component.html',
+    styleUrls: ['./pipeline-assembly-drawing-area-pan-zoom.component.scss'],
+})
+export class PipelineAssemblyDrawingAreaPanZoomComponent
+    implements OnInit, AfterViewInit, OnDestroy
+{
+    @Input()
+    jsplumbBridge: JsplumbBridge;
+
+    @Input()
+    pipelineCanvas: ElementRef;
+
+    panzoom: PanzoomObject;
+    moveSub: Subscription;
+    currentZoomLevel = 1;
+
+    constructor(
+        private pipelineElementDraggedService: PipelineElementDraggedService,
+    ) {}
+
+    ngOnInit() {
+        this.moveSub =
+            
this.pipelineElementDraggedService.pipelineElementMovedSubject.subscribe(
+                position => {
+                    const offsetHeight =
+                        this.pipelineCanvas.nativeElement.offsetHeight;
+                    const offsetWidth =
+                        this.pipelineCanvas.nativeElement.offsetWidth;
+                    const currentPan = this.panzoom.getPan();
+                    let xOffset = 0;
+                    let yOffset = 0;
+                    if (position.y + currentPan.y > offsetHeight - 100) {
+                        yOffset = -10;
+                    }
+                    if (position.x + currentPan.x > offsetWidth - 100) {
+                        xOffset = -10;
+                    }
+                    if (xOffset < 0 || yOffset < 0) {
+                        this.pan(xOffset, yOffset);
+                    }
+                },
+            );
+    }
+
+    ngAfterViewInit() {
+        const elem = document.getElementById('assembly');
+        this.panzoom = Panzoom(elem, {
+            maxScale: 5,
+            excludeClass: 'sp-no-pan',
+            canvas: true,
+            contain: 'outside',
+        });
+    }
+
+    zoomOut() {
+        this.doZoom(true);
+    }
+
+    zoomIn() {
+        this.doZoom(false);
+    }
+
+    doZoom(zoomOut) {
+        zoomOut ? this.panzoom.zoomOut() : this.panzoom.zoomIn();
+        this.currentZoomLevel = this.panzoom.getScale();
+        this.jsplumbBridge.setZoom(this.currentZoomLevel);
+        this.jsplumbBridge.repaintEverything();
+    }
+
+    panLeft() {
+        this.pan(100, 0);
+    }
+
+    panRight() {
+        this.pan(-100, 0);
+    }
+
+    panUp() {
+        this.pan(0, 100);
+    }
+
+    panDown() {
+        this.pan(0, -100);
+    }
+
+    panHome() {
+        this.panAbsolute(0, 0);
+    }
+
+    pan(xOffset: number, yOffset: number) {
+        const currentPan = this.panzoom.getPan();
+        const panX = Math.min(0, currentPan.x + xOffset);
+        const panY = Math.min(0, currentPan.y + yOffset);
+        this.panzoom.pan(panX, panY);
+    }
+
+    panAbsolute(x: number, y: number) {
+        this.panzoom.pan(x, y);
+    }
+
+    resetZoom(): void {
+        this.currentZoomLevel = 1;
+        this.jsplumbBridge.setZoom(this.currentZoomLevel);
+    }
+
+    ngOnDestroy() {
+        this.moveSub?.unsubscribe();
+    }
+}
diff --git 
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area.component.html
 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area.component.html
new file mode 100644
index 0000000000..98819f88e0
--- /dev/null
+++ 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area.component.html
@@ -0,0 +1,54 @@
+<!--
+  ~ 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.
+  ~
+  -->
+
+<div class="pipeline-validation-hint">
+    <sp-error-hint
+        *ngIf="!readonly"
+        [displayMessages]="!isPipelineAssemblyEmpty()"
+        [errorMessages]="pipelineValidationService.errorMessages"
+        [validationString]="'Pipeline'"
+    >
+    </sp-error-hint>
+</div>
+
+<sp-pipeline-assembly-drawing-area-pan-zoom
+    #zoomComponent
+    [pipelineCanvas]="pipelineCanvas"
+    [jsplumbBridge]="jsplumbBridge"
+>
+</sp-pipeline-assembly-drawing-area-pan-zoom>
+
+<div class="pipeline-canvas-outer" #outerCanvas>
+    <sp-pipeline
+        class="canvas jtk-surface"
+        id="assembly"
+        #pipelineComponent
+        [pipelineValid]="pipelineValid"
+        [rawPipelineModel]="rawPipelineModel"
+        [allElements]="allElements"
+        [readonly]="readonly"
+        [metricsInfo]="metricsInfo"
+        [previewModeActive]="previewModeActive"
+        [pipelinePreview]="pipelinePreview"
+        (triggerPipelineCacheUpdateEmitter)="
+            triggerPipelineCacheUpdateEmitter.emit()
+        "
+        style="position: absolute"
+    >
+    </sp-pipeline>
+</div>
diff --git a/ui/src/app/editor/components/pipeline/pipeline.component.scss 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area.component.scss
similarity index 56%
copy from ui/src/app/editor/components/pipeline/pipeline.component.scss
copy to 
ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area.component.scss
index 13cbc4aacb..41e539ef31 100644
--- a/ui/src/app/editor/components/pipeline/pipeline.component.scss
+++ 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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.
@@ -15,3 +15,37 @@
  * limitations under the License.
  *
  */
+
+.pipeline-validation-hint {
+    position: absolute;
+    right: 10px;
+    top: 20px;
+    z-index: 10;
+    background: var(--color-bg-1);
+    box-shadow: 0.175em 0.175em 0 0 rgba(15, 28, 63, 0.125);
+}
+
+.jtk-surface-nopan {
+    overflow: scroll !important;
+    cursor: default;
+}
+
+.pipeline-canvas-outer {
+    position: relative;
+    width: 100%;
+    height: 100%;
+}
+
+.canvas {
+    position: relative;
+    left: 0;
+    top: 0;
+    height: 3000px;
+    width: 4000px;
+    z-index: 0;
+    margin: -1px;
+
+    background-color: var(--color-bg-1);
+    background-image: var(--canvas-color);
+    background-size: 15px 15px;
+}
diff --git 
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area.component.ts
 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area.component.ts
new file mode 100644
index 0000000000..5169abe08e
--- /dev/null
+++ 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area.component.ts
@@ -0,0 +1,188 @@
+/*
+ * 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 {
+    Component,
+    ElementRef,
+    EventEmitter,
+    Input,
+    NgZone,
+    OnInit,
+    Output,
+    ViewChild,
+} from '@angular/core';
+import { JsplumbBridge } from '../../../services/jsplumb-bridge.service';
+import { PipelineComponent } from '../../pipeline/pipeline.component';
+import {
+    PipelineElementConfig,
+    PipelineElementUnion,
+} from '../../../model/editor.model';
+import { PipelineAssemblyDrawingAreaPanZoomComponent } from 
'./pipeline-assembly-drawing-area-pan-zoom/pipeline-assembly-drawing-area-pan-zoom.component';
+import { PipelineValidationService } from 
'../../../services/pipeline-validation.service';
+import {
+    PipelineCanvasMetadata,
+    PipelinePreviewModel,
+    SpMetricsEntry,
+} from '@streampipes/platform-services';
+import { EditorService } from '../../../services/editor.service';
+import { PipelinePositioningService } from 
'../../../services/pipeline-positioning.service';
+import { HttpDownloadProgressEvent } from '@angular/common/http';
+import { LivePreviewService } from '../../../../services/live-preview.service';
+import { ObjectProvider } from '../../../services/object-provider.service';
+import { Subscription } from 'rxjs';
+
+@Component({
+    selector: 'sp-pipeline-assembly-drawing-area',
+    templateUrl: './pipeline-assembly-drawing-area.component.html',
+    styleUrls: ['./pipeline-assembly-drawing-area.component.scss'],
+})
+export class PipelineAssemblyDrawingAreaComponent implements OnInit {
+    @Input()
+    jsplumbBridge: JsplumbBridge;
+
+    @Input()
+    allElements: PipelineElementUnion[];
+
+    @Input()
+    rawPipelineModel: PipelineElementConfig[];
+
+    @Input()
+    pipelineCanvasMetadata: PipelineCanvasMetadata;
+
+    @Input()
+    pipelineCanvasMetadataAvailable: boolean;
+
+    @Input()
+    previewModeActive: boolean;
+
+    @Input()
+    readonly: boolean;
+
+    @Input()
+    metricsInfo: Record<string, SpMetricsEntry>;
+
+    @Output()
+    triggerPipelineCacheUpdateEmitter: EventEmitter<void> = new EventEmitter();
+
+    pipelineValid = false;
+    pipelinePreview: PipelinePreviewModel;
+    pipelinePreviewSub: Subscription;
+
+    @ViewChild('pipelineComponent')
+    pipelineComponent: PipelineComponent;
+
+    @ViewChild('zoomComponent')
+    zoomComponent: PipelineAssemblyDrawingAreaPanZoomComponent;
+    @ViewChild('outerCanvas') pipelineCanvas: ElementRef;
+
+    constructor(
+        public pipelineValidationService: PipelineValidationService,
+        private ngZone: NgZone,
+        private editorService: EditorService,
+        private pipelinePositioningService: PipelinePositioningService,
+        private livePreviewService: LivePreviewService,
+        private objectProvider: ObjectProvider,
+    ) {}
+
+    ngOnInit(): void {
+        if (this.rawPipelineModel.length > 0) {
+            this.displayPipelineInEditor(
+                !this.pipelineCanvasMetadataAvailable,
+                this.pipelineCanvasMetadata,
+            );
+        }
+    }
+
+    isPipelineAssemblyEmpty() {
+        return (
+            this.rawPipelineModel.length === 0 ||
+            this.rawPipelineModel.every(pe => pe.settings.disabled)
+        );
+    }
+
+    displayPipelineInEditor(
+        autoLayout: boolean,
+        pipelineCanvasMetadata?: PipelineCanvasMetadata,
+    ): void {
+        setTimeout(() => {
+            this.pipelinePositioningService.displayPipeline(
+                this.rawPipelineModel,
+                '#assembly',
+                false,
+                autoLayout,
+                pipelineCanvasMetadata,
+            );
+
+            this.editorService.makePipelineAssemblyEmpty(false);
+            this.ngZone.run(() => {
+                this.pipelineValid =
+                    this.pipelineValidationService.isValidPipeline(
+                        this.rawPipelineModel.filter(
+                            pe => !pe.settings.disabled,
+                        ),
+                        false,
+                    );
+            });
+            if (!this.readonly) {
+                this.pipelineComponent.triggerPipelineModification();
+            }
+        });
+    }
+
+    initiatePipelineElementPreview() {
+        if (!this.previewModeActive) {
+            const pipeline = this.objectProvider.makePipeline(
+                this.rawPipelineModel,
+            );
+            this.editorService
+                .initiatePipelinePreview(pipeline)
+                .subscribe(response => {
+                    this.pipelinePreview = response;
+                    this.previewModeActive = true;
+                    this.pipelinePreviewSub = this.editorService
+                        .getPipelinePreviewResult(response.previewId)
+                        .subscribe(res => {
+                            const data = this.livePreviewService.convert(
+                                res as HttpDownloadProgressEvent,
+                            );
+                            this.livePreviewService.eventSub.next(data);
+                        });
+                });
+        } else {
+            this.deletePipelineElementPreview(false);
+        }
+    }
+
+    deletePipelineElementPreview(resume: boolean) {
+        if (this.previewModeActive) {
+            this.pipelinePreviewSub?.unsubscribe();
+            this.editorService
+                .deletePipelinePreviewRequest(this.pipelinePreview.previewId)
+                .subscribe(() => {
+                    this.previewModeActive = false;
+                    if (resume) {
+                        this.initiatePipelineElementPreview();
+                    }
+                });
+        }
+    }
+
+    resetZoom(): void {
+        this.zoomComponent.resetZoom();
+    }
+}
diff --git 
a/ui/src/app/pipeline-details/components/preview/pipeline-preview.component.html
 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options-pipeline-cache/pipeline-assembly-options-pipeline-cache.component.html
similarity index 53%
copy from 
ui/src/app/pipeline-details/components/preview/pipeline-preview.component.html
copy to 
ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options-pipeline-cache/pipeline-assembly-options-pipeline-cache.component.html
index ca4a4fc397..7a450fafce 100644
--- 
a/ui/src/app/pipeline-details/components/preview/pipeline-preview.component.html
+++ 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options-pipeline-cache/pipeline-assembly-options-pipeline-cache.component.html
@@ -16,16 +16,31 @@
   ~
   -->
 
-<div class="outer-assembly-preview">
-    <div class="pipeline-canvas-outer canvas-preview-inner">
-        <div id="{{ jspcanvas }}" class="canvas-preview">
-            <sp-pipeline
-                [canvasId]="jspcanvas"
-                [metricsInfo]="metricsInfo"
-                [rawPipelineModel]="rawPipelineModel"
-                [preview]="true"
-            ></sp-pipeline>
+<div
+    fxLayout="column"
+    fxLayoutAlign="start center"
+    class="pipeline-cache-block"
+>
+    <div
+        fxFlex="100"
+        fxLayoutAlign="start center"
+        fxLayout="row"
+        *ngIf="pipelineCached || pipelineCacheRunning"
+    >
+        <div fxLayout="row" fxLayoutAlign="start center">
+            <mat-spinner
+                [mode]="'indeterminate'"
+                color="accent"
+                [diameter]="15"
+                *ngIf="pipelineCacheRunning"
+            ></mat-spinner>
+            <span
+                >&nbsp;{{
+                    pipelineCacheRunning
+                        ? '&nbsp;Saving pipeline modifications'
+                        : 'All pipeline modifications saved.'
+                }}</span
+            >
         </div>
     </div>
-    <ng-content></ng-content>
 </div>
diff --git a/ui/src/app/editor/components/pipeline/pipeline.component.scss 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options-pipeline-cache/pipeline-assembly-options-pipeline-cache.component.scss
similarity index 86%
copy from ui/src/app/editor/components/pipeline/pipeline.component.scss
copy to 
ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options-pipeline-cache/pipeline-assembly-options-pipeline-cache.component.scss
index 13cbc4aacb..28eafec0a9 100644
--- a/ui/src/app/editor/components/pipeline/pipeline.component.scss
+++ 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options-pipeline-cache/pipeline-assembly-options-pipeline-cache.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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.
@@ -15,3 +15,12 @@
  * limitations under the License.
  *
  */
+
+.pipeline-cache-progress {
+    display: inline-block;
+}
+
+.pipeline-cache-block {
+    height: 100%;
+    margin-left: 15px;
+}
diff --git 
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options-pipeline-cache/pipeline-assembly-options-pipeline-cache.component.ts
 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options-pipeline-cache/pipeline-assembly-options-pipeline-cache.component.ts
new file mode 100644
index 0000000000..5784af4cea
--- /dev/null
+++ 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options-pipeline-cache/pipeline-assembly-options-pipeline-cache.component.ts
@@ -0,0 +1,66 @@
+/*
+ * 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 { Component, Input } from '@angular/core';
+import { PipelineElementConfig } from '../../../../model/editor.model';
+import { forkJoin } from 'rxjs';
+import { PipelinePositioningService } from 
'../../../../services/pipeline-positioning.service';
+import { EditorService } from '../../../../services/editor.service';
+import { PipelineCanvasMetadata } from '@streampipes/platform-services';
+
+@Component({
+    selector: 'sp-pipeline-assembly-options-pipeline-cache',
+    templateUrl: './pipeline-assembly-options-pipeline-cache.component.html',
+    styleUrls: ['./pipeline-assembly-options-pipeline-cache.component.scss'],
+})
+export class PipelineAssemblyOptionsPipelineCacheComponent {
+    pipelineCached = false;
+    pipelineCacheRunning = false;
+
+    @Input()
+    rawPipelineModel: PipelineElementConfig[];
+
+    @Input()
+    pipelineCanvasMetadata: PipelineCanvasMetadata;
+
+    constructor(
+        private pipelinePositioningService: PipelinePositioningService,
+        private editorService: EditorService,
+    ) {}
+
+    triggerPipelineCacheUpdate() {
+        setTimeout(() => {
+            this.pipelineCacheRunning = true;
+            this.pipelineCached = false;
+            this.pipelineCanvasMetadata =
+                
this.pipelinePositioningService.collectPipelineElementPositions(
+                    this.pipelineCanvasMetadata,
+                    this.rawPipelineModel,
+                );
+            forkJoin([
+                this.editorService.updateCachedPipeline(this.rawPipelineModel),
+                this.editorService.updateCachedCanvasMetadata(
+                    this.pipelineCanvasMetadata,
+                ),
+            ]).subscribe(() => {
+                this.pipelineCacheRunning = false;
+                this.pipelineCached = true;
+            });
+        });
+    }
+}
diff --git 
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.html
 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.html
new file mode 100644
index 0000000000..bbef21c3be
--- /dev/null
+++ 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.html
@@ -0,0 +1,89 @@
+<!--
+  ~ 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.
+  ~
+  -->
+
+<div fxFlex="100" fxLayout="row" fxLayoutAlign="start center">
+    <button
+        mat-button
+        mat-raised-button
+        color="accent"
+        matTooltip="Save Pipeline"
+        [matTooltipPosition]="'above'"
+        [disabled]="!pipelineValidationService.pipelineValid"
+        (click)="savePipelineEmitter.emit()"
+        type="submit"
+        data-cy="sp-editor-save-pipeline"
+    >
+        <div fxLayoutAlign="start center" fxLayout="row">
+            <i class="material-icons">save</i>
+            <span>&nbsp;Save pipeline</span>
+        </div>
+    </button>
+    <span class="assembly-options-divider"></span>
+    <button
+        mat-button
+        color="accent"
+        matTooltip="Data Preview"
+        [matTooltipPosition]="'above'"
+        (click)="togglePreviewEmitter.emit()"
+        [disabled]="isPipelineAssemblyEmpty()"
+    >
+        <div fxLayoutAlign="start center" fxLayout="row">
+            <i class="material-icons">visibility</i>
+            <span *ngIf="!previewModeActive">&nbsp;Enable live preview</span>
+            <span *ngIf="previewModeActive">&nbsp;Disable live preview</span>
+        </div>
+    </button>
+    <span class="assembly-options-divider"></span>
+    <button
+        color="accent"
+        mat-icon-button
+        matTooltip="Auto Layout"
+        [matTooltipPosition]="'above'"
+        (click)="autoLayout()"
+    >
+        <i class="material-icons">settings_overscan</i>
+    </button>
+    <button
+        color="accent"
+        mat-icon-button
+        matTooltip="Add pipeline element"
+        [matTooltipPosition]="'above'"
+        (click)="openDiscoverDialog()"
+        data-cy="sp-editor-add-pipeline-element"
+    >
+        <i class="material-icons">add</i>
+    </button>
+    <sp-pipeline-assembly-options-pipeline-cache
+        [rawPipelineModel]="rawPipelineModel"
+        [pipelineCanvasMetadata]="pipelineCanvasMetadata"
+        #assemblyOptionsPipelineCacheComponent
+    >
+    </sp-pipeline-assembly-options-pipeline-cache>
+    <span fxFlex></span>
+    <button
+        color="accent"
+        mat-icon-button
+        matTooltip="Clear Assembly Area"
+        [matTooltipPosition]="'above'"
+        [disabled]="editorService.pipelineAssemblyEmpty"
+        (click)="showClearAssemblyConfirmDialog($event)"
+        data-cy="clear-assembly-area"
+    >
+        <i class="material-icons">clear</i>
+    </button>
+</div>
diff --git a/ui/src/app/editor/components/pipeline/pipeline.component.scss 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.scss
similarity index 84%
rename from ui/src/app/editor/components/pipeline/pipeline.component.scss
rename to 
ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.scss
index 13cbc4aacb..7f7266f016 100644
--- a/ui/src/app/editor/components/pipeline/pipeline.component.scss
+++ 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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.
@@ -15,3 +15,11 @@
  * limitations under the License.
  *
  */
+
+.assembly-options-divider {
+    width: 1px;
+    height: 70%;
+    margin-left: 10px;
+    margin-right: 10px;
+    background: var(--color-bg-3);
+}
diff --git 
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.ts
 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.ts
new file mode 100644
index 0000000000..00ecb8e93b
--- /dev/null
+++ 
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.ts
@@ -0,0 +1,135 @@
+/*
+ * 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 {
+    Component,
+    EventEmitter,
+    Input,
+    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';
+import {
+    ConfirmDialogComponent,
+    DialogService,
+    PanelType,
+} from '@streampipes/shared-ui';
+import { EditorService } from '../../../services/editor.service';
+import { MatDialog } from '@angular/material/dialog';
+import { PipelineElementDiscoveryComponent } from 
'../../../dialog/pipeline-element-discovery/pipeline-element-discovery.component';
+import {
+    PipelineElementConfig,
+    PipelineElementUnion,
+} from '../../../model/editor.model';
+import { PipelineAssemblyOptionsPipelineCacheComponent } from 
'./pipeline-assembly-options-pipeline-cache/pipeline-assembly-options-pipeline-cache.component';
+import { PipelineCanvasMetadata } from '@streampipes/platform-services';
+
+@Component({
+    selector: 'sp-pipeline-assembly-options',
+    templateUrl: './pipeline-assembly-options.component.html',
+    styleUrls: ['./pipeline-assembly-options.component.scss'],
+})
+export class PipelineAssemblyOptionsComponent {
+    @Input()
+    jsplumbBridge: JsplumbBridge;
+
+    @Input()
+    rawPipelineModel: PipelineElementConfig[];
+
+    @Input()
+    pipelineCanvasMetadata: PipelineCanvasMetadata;
+
+    @Input()
+    allElements: PipelineElementUnion[];
+
+    @Input()
+    previewModeActive: boolean;
+
+    @Output()
+    savePipelineEmitter: EventEmitter<void> = new EventEmitter<void>();
+
+    @Output()
+    clearAssemblyEmitter: EventEmitter<void> = new EventEmitter<void>();
+
+    @Output()
+    togglePreviewEmitter: EventEmitter<void> = new EventEmitter<void>();
+
+    @ViewChild('assemblyOptionsPipelineCacheComponent')
+    assemblyOptionsCacheComponent: 
PipelineAssemblyOptionsPipelineCacheComponent;
+
+    constructor(
+        public editorService: EditorService,
+        public pipelineValidationService: PipelineValidationService,
+        private pipelinePositioningService: PipelinePositioningService,
+        private dialog: MatDialog,
+        private dialogService: DialogService,
+    ) {}
+
+    autoLayout() {
+        this.pipelinePositioningService.layoutGraph(
+            '#assembly',
+            "div[id^='jsplumb']",
+            110,
+            false,
+        );
+        this.jsplumbBridge.repaintEverything();
+    }
+
+    openDiscoverDialog() {
+        this.dialogService.open(PipelineElementDiscoveryComponent, {
+            panelType: PanelType.SLIDE_IN_PANEL,
+            title: 'Discover pipeline elements',
+            width: '50vw',
+            data: {
+                currentElements: this.allElements,
+                rawPipelineModel: this.rawPipelineModel,
+            },
+        });
+    }
+
+    showClearAssemblyConfirmDialog(event: any) {
+        const dialogRef = this.dialog.open(ConfirmDialogComponent, {
+            width: '500px',
+            data: {
+                title: 'Do you really want to delete the current pipeline?',
+                subtitle: 'This cannot be undone.',
+                cancelTitle: 'No',
+                okTitle: 'Yes',
+                confirmAndCancel: true,
+            },
+        });
+        dialogRef.afterClosed().subscribe(ev => {
+            if (ev) {
+                this.clearAssemblyEmitter.emit();
+            }
+        });
+    }
+
+    isPipelineAssemblyEmpty() {
+        return (
+            this.rawPipelineModel.length === 0 ||
+            this.rawPipelineModel.every(pe => pe.settings.disabled)
+        );
+    }
+
+    triggerCacheUpdate(): void {
+        this.assemblyOptionsCacheComponent.triggerPipelineCacheUpdate();
+    }
+}
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 592cc32b32..e7d49b22b8 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
@@ -18,176 +18,32 @@
 
 <div fxFlex="100" fxLayout="column">
     <div class="pipeline-assembly-options page-container-nav">
-        <div fxFlex="100" fxLayout="row" fxLayoutAlign="start center">
-            <button
-                mat-button
-                mat-raised-button
-                color="accent"
-                matTooltip="Save Pipeline"
-                [matTooltipPosition]="'above'"
-                [disabled]="!pipelineValidationService.pipelineValid"
-                (click)="submit()"
-                type="submit"
-                data-cy="sp-editor-save-pipeline"
-            >
-                <div fxLayoutAlign="start center" fxLayout="row">
-                    <i class="material-icons">save</i>
-                    <span>&nbsp;Save pipeline</span>
-                </div>
-            </button>
-            <span class="assembly-options-divider"></span>
-            <!-- TODO: Use this once copying of elements is supported -->
-            <!--            <button mat-button mat-icon-button 
matTooltip="Pan" [matTooltipPosition]="'above'"-->
-            <!--                    [disabled]="!selectMode"-->
-            <!--                    (click)="toggleSelectMode()">-->
-            <!--                <i class="material-icons">open_with</i>-->
-            <!--            </button>-->
-            <!--            <button mat-button mat-icon-button 
matTooltip="Select" [matTooltipPosition]="'above'"-->
-            <!--                    [disabled]="selectMode"-->
-            <!--                    (click)="toggleSelectMode()">-->
-            <!--                <i class="material-icons">mode_edit</i>-->
-            <!--            </button>-->
-            <button
-                mat-button
-                color="accent"
-                matTooltip="Data Preview"
-                [matTooltipPosition]="'above'"
-                (click)="triggerPipelinePreview()"
-                [disabled]="isPipelineAssemblyEmpty()"
-            >
-                <div fxLayoutAlign="start center" fxLayout="row">
-                    <i class="material-icons">visibility</i>
-                    <span *ngIf="!pipelineComponent.previewModeActive"
-                        >&nbsp;Enable live preview</span
-                    >
-                    <span *ngIf="pipelineComponent.previewModeActive"
-                        >&nbsp;Disable live preview</span
-                    >
-                </div>
-            </button>
-            <span class="assembly-options-divider"></span>
-            <button
-                color="accent"
-                mat-icon-button
-                matTooltip="Auto Layout"
-                [matTooltipPosition]="'above'"
-                (click)="autoLayout()"
-            >
-                <i class="material-icons">settings_overscan</i>
-            </button>
-            <button
-                color="accent"
-                mat-icon-button
-                matTooltip="Add pipeline element"
-                [matTooltipPosition]="'above'"
-                (click)="openDiscoverDialog()"
-                data-cy="sp-editor-add-pipeline-element"
-            >
-                <i class="material-icons">add</i>
-            </button>
-            <div
-                fxLayout="column"
-                fxLayoutAlign="start center"
-                class="pipeline-cache-block"
-            >
-                <div
-                    fxFlex="100"
-                    fxLayoutAlign="start center"
-                    fxLayout="row"
-                    *ngIf="pipelineCached || pipelineCacheRunning"
-                >
-                    <div
-                        *ngIf="pipelineCached"
-                        fxLayout="row"
-                        fxLayoutAlign="start center"
-                    >
-                        <mat-spinner
-                            [mode]="'indeterminate'"
-                            class="mat-spinner-color"
-                            [diameter]="15"
-                            *ngIf="pipelineCacheRunning"
-                        ></mat-spinner>
-                        <span
-                            >&nbsp;{{
-                                pipelineCacheRunning
-                                    ? '&nbsp;Saving pipeline modifications'
-                                    : 'All pipeline modifications saved.'
-                            }}</span
-                        >
-                    </div>
-                </div>
-            </div>
-            <span fxFlex></span>
-            <button
-                color="accent"
-                mat-icon-button
-                matTooltip="Clear Assembly Area"
-                [matTooltipPosition]="'above'"
-                [disabled]="editorService.pipelineAssemblyEmpty"
-                (click)="showClearAssemblyConfirmDialog($event)"
-                data-cy="clear-assembly-area"
-            >
-                <i class="material-icons">clear</i>
-            </button>
-        </div>
+        <sp-pipeline-assembly-options
+            #assemblyOptionsComponent
+            [rawPipelineModel]="rawPipelineModel"
+            [pipelineCanvasMetadata]="pipelineCanvasMetadata"
+            [allElements]="allElements"
+            [jsplumbBridge]="JsplumbBridge"
+            [previewModeActive]="previewModeActive"
+            (clearAssemblyEmitter)="clearAssembly()"
+            (togglePreviewEmitter)="togglePreview()"
+            (savePipelineEmitter)="submit()"
+        >
+        </sp-pipeline-assembly-options>
     </div>
     <div id="outerAssemblyArea" class="outerAssembly assembly-border">
-        <div class="pipeline-canvas-outer" #outerCanvas>
-            <div class="pipeline-validation-hint">
-                <sp-error-hint
-                    [displayMessages]="!isPipelineAssemblyEmpty()"
-                    [errorMessages]="pipelineValidationService.errorMessages"
-                    [validationString]="'Pipeline'"
-                >
-                </sp-error-hint>
-            </div>
-            <div class="pan-control">
-                <div class="pan-zoom-control-buttons">
-                    <div class="pan-zoom-button pan-left" (click)="panLeft()">
-                        <i class="material-icons">keyboard_arrow_left</i>
-                    </div>
-                    <div class="pan-zoom-button pan-right" 
(click)="panRight()">
-                        <i class="material-icons">keyboard_arrow_right</i>
-                    </div>
-                    <div class="pan-zoom-button pan-home" (click)="panHome()">
-                        <i class="material-icons">home</i>
-                    </div>
-                    <div class="pan-zoom-button pan-up" (click)="panUp()">
-                        <i class="material-icons">keyboard_arrow_up</i>
-                    </div>
-                    <div class="pan-zoom-button pan-down" (click)="panDown()">
-                        <i class="material-icons">keyboard_arrow_down</i>
-                    </div>
-                </div>
-            </div>
-            <div class="zoom-control">
-                <div class="pan-zoom-control-buttons">
-                    <div class="pan-zoom-button zoom-in" (click)="zoomIn()">
-                        <i class="material-icons">zoom_in</i>
-                    </div>
-                    <mat-divider class="zoom-divider"></mat-divider>
-                    <div class="pan-zoom-button zoom-out" (click)="zoomOut()">
-                        <i class="material-icons">zoom_out</i>
-                    </div>
-                </div>
-            </div>
-            <sp-pipeline
-                class="canvas jtk-surface"
-                id="assembly"
-                #pipelineComponent
-                [pipelineValid]="pipelineValid"
-                [canvasId]="'assembly'"
-                [rawPipelineModel]="rawPipelineModel"
-                [allElements]="allElements"
-                [preview]="false"
-                [pipelineCached]="pipelineCached"
-                [pipelineCanvasMetadata]="pipelineCanvasMetadata"
-                [pipelineCacheRunning]="pipelineCacheRunning"
-                (pipelineCachedChanged)="pipelineCached = $event"
-                (pipelineCacheRunningChanged)="pipelineCacheRunning = $event"
-                style="position: absolute"
-            >
-            </sp-pipeline>
-        </div>
+        <sp-pipeline-assembly-drawing-area
+            #drawingAreaComponent
+            fxFlex="100"
+            style="position: relative"
+            (triggerPipelineCacheUpdateEmitter)="triggerCacheUpdate()"
+            [readonly]="readonly"
+            [allElements]="allElements"
+            [rawPipelineModel]="rawPipelineModel"
+            [pipelineCanvasMetadata]="pipelineCanvasMetadata"
+            [pipelineCanvasMetadataAvailable]="pipelineCanvasMetadataAvailable"
+            [jsplumbBridge]="JsplumbBridge"
+        >
+        </sp-pipeline-assembly-drawing-area>
     </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 7d714593ef..5ea0cbc62b 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
@@ -22,13 +22,9 @@
     stroke: var(--color-accent) !important;
 }
 
-.pipeline-cache-progress {
-    display: inline-block;
-}
-
-.pipeline-cache-block {
-    height: 100%;
-    margin-left: 15px;
+.pipeline-assembly-options {
+    padding-left: 5px;
+    border: 1px solid var(--color-bg-3);
 }
 
 .outerAssembly {
@@ -39,133 +35,10 @@
     overflow: hidden;
 }
 
-.pipeline-canvas-outer {
-    position: relative;
-    width: 100%;
-    height: 100%;
-}
-
-.canvas {
-    //overflow: scroll;
-    position: relative;
-    left: 0px;
-    top: 0px;
-    height: 3000px;
-    width: 4000px;
-    z-index: 0;
-    margin: -1px;
-
-    background-color: var(--color-bg-1);
-    background-image: var(--canvas-color);
-    background-size: 15px 15px;
-}
-
-.pipeline-assembly-options {
-    padding-left: 5px;
-    border: 1px solid var(--color-bg-3);
-    //background: #f6f6f6;
-}
-
 .assembly-border {
     border: 1px solid var(--color-bg-3);
 }
 
-.jtk-surface-nopan {
-    overflow: scroll !important;
-    cursor: default;
-}
-
-.zoom-control {
-    position: absolute;
-    right: 10px;
-    bottom: 10px;
-    width: 40px;
-    height: 80px;
-    z-index: 10;
-    background: var(--color-bg-1);
-    box-shadow: 0.175em 0.175em 0 0 rgba(15, 28, 63, 0.125);
-    border-radius: 10px;
-}
-
-.pipeline-validation-hint {
-    position: absolute;
-    right: 10px;
-    top: 20px;
-    z-index: 10;
-    background: var(--color-bg-1);
-    box-shadow: 0.175em 0.175em 0 0 rgba(15, 28, 63, 0.125);
-}
-
-.pan-control {
-    position: absolute;
-    right: 60px;
-    bottom: 10px;
-    width: 80px;
-    height: 80px;
-    background: var(--color-bg-1);
-    box-shadow: 0.175em 0.175em 0 0 rgba(15, 28, 63, 0.125);
-    border-radius: 50%;
-    z-index: 10;
-}
-
-.pan-zoom-button {
-    position: absolute;
-    cursor: pointer;
-    color: var(--color-accent);
-}
-
-.pan-zoom-control-buttons {
-    position: relative;
-}
-
-.zoom-divider {
-    top: 40px;
-    position: relative;
-}
-
-.zoom-in {
-    top: 8px;
-    left: 8px;
-}
-
-.zoom-out {
-    top: 48px;
-    left: 8px;
-}
-
-.pan-up {
-    left: 28px;
-    top: 2px;
-}
-
-.pan-down {
-    left: 28px;
-    top: 58px;
-}
-
-.pan-left {
-    left: -3px;
-    top: 30px;
-}
-
-.pan-right {
-    left: 55px;
-    top: 30px;
-}
-
-.pan-home {
-    left: 28px;
-    top: 30px;
-}
-
-.assembly-options-divider {
-    width: 1px;
-    height: 70%;
-    margin-left: 10px;
-    margin-right: 10px;
-    background: var(--color-bg-3);
-}
-
 .color-warn {
     color: var(--color-warn);
 }
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 fa5c9afa1e..3486572eaa 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,96 +16,58 @@
  *
  */
 
-import {
-    AfterViewInit,
-    Component,
-    ElementRef,
-    EventEmitter,
-    Input,
-    NgZone,
-    OnDestroy,
-    OnInit,
-    Output,
-    ViewChild,
-} from '@angular/core';
+import { AfterViewInit, Component, Input, 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';
-import { JsplumbService } from '../../services/jsplumb.service';
 import {
     PipelineElementConfig,
     PipelineElementUnion,
 } from '../../model/editor.model';
 import { ObjectProvider } from '../../services/object-provider.service';
-import {
-    ConfirmDialogComponent,
-    DialogService,
-    PanelType,
-    SpBreadcrumbService,
-} from '@streampipes/shared-ui';
+import { DialogService, PanelType } from '@streampipes/shared-ui';
 import { SavePipelineComponent } from 
'../../dialog/save-pipeline/save-pipeline.component';
-import { MatDialog } from '@angular/material/dialog';
 import { EditorService } from '../../services/editor.service';
 import {
     Pipeline,
     PipelineCanvasMetadata,
-    PipelineCanvasMetadataService,
-    PipelineService,
 } from '@streampipes/platform-services';
 import { JsplumbFactoryService } from '../../services/jsplumb-factory.service';
-import Panzoom, { PanzoomObject } from '@panzoom/panzoom';
-import { PipelineElementDraggedService } from 
'../../services/pipeline-element-dragged.service';
-import { PipelineComponent } from '../pipeline/pipeline.component';
-import { forkJoin, of, Subscription } from 'rxjs';
-import { PipelineElementDiscoveryComponent } from 
'../../dialog/pipeline-element-discovery/pipeline-element-discovery.component';
-import { SpPipelineRoutes } from '../../../pipelines/pipelines.routes';
+import { forkJoin } from 'rxjs';
 import { Router } from '@angular/router';
-import { catchError } from 'rxjs/operators';
+import { PipelineAssemblyDrawingAreaComponent } from 
'./pipeline-assembly-drawing-area/pipeline-assembly-drawing-area.component';
+import { PipelineAssemblyOptionsComponent } from 
'./pipeline-assembly-options/pipeline-assembly-options.component';
 
 @Component({
     selector: 'sp-pipeline-assembly',
     templateUrl: './pipeline-assembly.component.html',
     styleUrls: ['./pipeline-assembly.component.scss'],
 })
-export class PipelineAssemblyComponent
-    implements OnInit, AfterViewInit, OnDestroy
-{
+export class PipelineAssemblyComponent implements AfterViewInit {
     @Input()
     rawPipelineModel: PipelineElementConfig[];
 
     @Input()
-    currentModifiedPipelineId: string;
-
-    @Input()
-    allElements: PipelineElementUnion[];
-
-    @Output()
-    pipelineCanvasMaximizedEmitter: EventEmitter<boolean> =
-        new EventEmitter<boolean>();
-
-    JsplumbBridge: JsplumbBridge;
-    currentZoomLevel: any;
-    preview: any;
-
-    selectMode: any;
     originalPipeline: Pipeline;
-    pipelineValid = false;
 
-    pipelineCacheRunning = false;
-    pipelineCached = false;
+    @Input()
+    pipelineCanvasMetadata: PipelineCanvasMetadata;
 
-    pipelineCanvasMetadata: PipelineCanvasMetadata =
-        new PipelineCanvasMetadata();
+    @Input()
     pipelineCanvasMetadataAvailable = false;
 
-    config: any = {};
-    @ViewChild('outerCanvas') pipelineCanvas: ElementRef;
+    @Input()
+    allElements: PipelineElementUnion[];
 
-    @ViewChild('pipelineComponent')
-    pipelineComponent: PipelineComponent;
+    previewModeActive = false;
+    readonly: boolean;
 
-    panzoom: PanzoomObject;
-    moveSub: Subscription;
+    JsplumbBridge: JsplumbBridge;
+
+    @ViewChild('assemblyOptionsComponent')
+    assemblyOptionsComponent: PipelineAssemblyOptionsComponent;
+    @ViewChild('drawingAreaComponent')
+    drawingAreaComponent: PipelineAssemblyDrawingAreaComponent;
 
     constructor(
         private jsPlumbFactoryService: JsplumbFactoryService,
@@ -113,125 +75,34 @@ export class PipelineAssemblyComponent
         private objectProvider: ObjectProvider,
         public editorService: EditorService,
         public pipelineValidationService: PipelineValidationService,
-        private pipelineService: PipelineService,
-        private jsplumbService: JsplumbService,
         private dialogService: DialogService,
-        private dialog: MatDialog,
-        private ngZone: NgZone,
         private router: Router,
-        private pipelineElementDraggedService: PipelineElementDraggedService,
-        private pipelineCanvasMetadataService: PipelineCanvasMetadataService,
-        private breadcrumbService: SpBreadcrumbService,
-    ) {
-        this.selectMode = true;
-        this.currentZoomLevel = 1;
-    }
-
-    ngOnInit(): void {
-        if (this.currentModifiedPipelineId) {
-            this.displayPipelineById(this.currentModifiedPipelineId);
-        } else {
-            this.checkAndDisplayCachedPipeline();
-        }
-        this.moveSub =
-            
this.pipelineElementDraggedService.pipelineElementMovedSubject.subscribe(
-                position => {
-                    const offsetHeight =
-                        this.pipelineCanvas.nativeElement.offsetHeight;
-                    const offsetWidth =
-                        this.pipelineCanvas.nativeElement.offsetWidth;
-                    const currentPan = this.panzoom.getPan();
-                    let xOffset = 0;
-                    let yOffset = 0;
-                    if (position.y + currentPan.y > offsetHeight - 100) {
-                        yOffset = -10;
-                    }
-                    if (position.x + currentPan.x > offsetWidth - 100) {
-                        xOffset = -10;
-                    }
-                    if (xOffset < 0 || yOffset < 0) {
-                        this.pan(xOffset, yOffset);
-                    }
-                },
-            );
-    }
+    ) {}
 
     ngAfterViewInit() {
         this.JsplumbBridge = this.jsPlumbFactoryService.getJsplumbBridge(
-            this.preview,
+            this.readonly,
         );
-        const elem = document.getElementById('assembly');
-        this.panzoom = Panzoom(elem, {
-            maxScale: 5,
-            excludeClass: 'sp-no-pan',
-            canvas: true,
-            contain: 'outside',
-        });
-    }
-
-    autoLayout() {
-        this.pipelinePositioningService.layoutGraph(
-            '#assembly',
-            "div[id^='jsplumb']",
-            110,
-            false,
-        );
-        this.JsplumbBridge.repaintEverything();
-    }
-
-    zoomOut() {
-        this.doZoom(true);
-    }
-
-    zoomIn() {
-        this.doZoom(false);
-    }
-
-    doZoom(zoomOut) {
-        zoomOut ? this.panzoom.zoomOut() : this.panzoom.zoomIn();
-        this.currentZoomLevel = this.panzoom.getScale();
-        this.JsplumbBridge.setZoom(this.currentZoomLevel);
-        this.JsplumbBridge.repaintEverything();
-    }
-
-    showClearAssemblyConfirmDialog(event: any) {
-        const dialogRef = this.dialog.open(ConfirmDialogComponent, {
-            width: '500px',
-            data: {
-                title: 'Do you really want to delete the current pipeline?',
-                subtitle: 'This cannot be undone.',
-                cancelTitle: 'No',
-                okTitle: 'Yes',
-                confirmAndCancel: true,
-            },
-        });
-        dialogRef.afterClosed().subscribe(ev => {
-            if (ev) {
-                if (this.currentModifiedPipelineId) {
-                    this.currentModifiedPipelineId = undefined;
-                }
-                this.clearAssembly();
-                this.editorService.makePipelineAssemblyEmpty(true);
-            }
-        });
     }
 
     /**
      * clears the Assembly of all elements
      */
     clearAssembly() {
+        this.editorService.makePipelineAssemblyEmpty(true);
         this.JsplumbBridge.deleteEveryEndpoint();
         this.rawPipelineModel = [];
-        this.currentZoomLevel = 1;
-        this.JsplumbBridge.setZoom(this.currentZoomLevel);
+        this.drawingAreaComponent.resetZoom();
         this.JsplumbBridge.repaintEverything();
 
         forkJoin([
             this.editorService.removePipelineFromCache(),
             this.editorService.removeCanvasMetadataFromCache(),
-        ]).subscribe(msg => {
-            this.pipelineCached = false;
-            this.pipelineCacheRunning = false;
+        ]).subscribe(() => {
+            this.pipelineCanvasMetadata = new PipelineCanvasMetadata();
+            if (this.originalPipeline) {
+                this.router.navigate(['pipelines', 'create']);
+            }
         });
     }
 
@@ -239,7 +110,8 @@ export class PipelineAssemblyComponent
      * Sends the pipeline to the server
      */
     submit() {
-        const pipelineModel = this.pipelineComponent.rawPipelineModel;
+        //const pipelineModel = this.pipelineComponent.rawPipelineModel;
+        const pipelineModel = this.rawPipelineModel;
         const pipeline = this.objectProvider.makePipeline(pipelineModel);
         this.pipelinePositioningService.collectPipelineElementPositions(
             this.pipelineCanvasMetadata,
@@ -247,7 +119,7 @@ export class PipelineAssemblyComponent
         );
         pipeline.valid = this.pipelineValidationService.isValidPipeline(
             pipelineModel,
-            this.preview,
+            this.readonly,
         );
         const dialogRef = this.dialogService.open(SavePipelineComponent, {
             panelType: PanelType.SLIDE_IN_PANEL,
@@ -265,9 +137,7 @@ export class PipelineAssemblyComponent
             .subscribe((config: { reload: boolean; pipelineId: string }) => {
                 if (config?.reload) {
                     this.clearAssembly();
-                    this.editorService.makePipelineAssemblyEmpty(true);
                     this.rawPipelineModel = [];
-                    this.currentModifiedPipelineId = undefined;
                     setTimeout(() => {
                         this.router.navigate(['pipelines', 'create']);
                     });
@@ -275,149 +145,12 @@ export class PipelineAssemblyComponent
             });
     }
 
-    checkAndDisplayCachedPipeline() {
-        const cachedPipeline = this.editorService.getCachedPipeline();
-        const cachedCanvasMetadata =
-            this.editorService.getCachedPipelineCanvasMetadata();
-        forkJoin([cachedPipeline, cachedCanvasMetadata]).subscribe(results => {
-            if (results[0] && results[0].length > 0) {
-                this.rawPipelineModel = results[0] as PipelineElementConfig[];
-                this.handleCanvasMetadataResponse(results[1]);
-                this.displayPipelineInEditor(
-                    !this.pipelineCanvasMetadataAvailable,
-                    this.pipelineCanvasMetadata,
-                );
-            }
-        });
-    }
-
-    displayPipelineById(pipelineId: string) {
-        const pipelineReq = this.pipelineService.getPipelineById(pipelineId);
-        const canvasMetadataReq = this.pipelineCanvasMetadataService
-            .getPipelineCanvasMetadata(pipelineId)
-            .pipe(
-                catchError(error => {
-                    this.handleCanvasMetadataResponse(undefined);
-                    return of(undefined);
-                }),
-            );
-
-        forkJoin([pipelineReq, canvasMetadataReq]).subscribe(
-            ([pipelineResp, canvasResp]) => {
-                if (pipelineResp) {
-                    this.originalPipeline = pipelineResp;
-                    this.breadcrumbService.updateBreadcrumb([
-                        SpPipelineRoutes.BASE,
-                        { label: this.originalPipeline.name },
-                        { label: 'Modify' },
-                    ]);
-                    this.rawPipelineModel = 
this.jsplumbService.makeRawPipeline(
-                        this.originalPipeline,
-                        false,
-                    );
-                }
-                if (canvasResp !== undefined) {
-                    this.handleCanvasMetadataResponse(canvasResp);
-                }
-                this.displayPipelineInEditor(
-                    !this.pipelineCanvasMetadataAvailable,
-                    this.pipelineCanvasMetadata,
-                );
-            },
-        );
-    }
-
-    handleCanvasMetadataResponse(canvasMetadata: PipelineCanvasMetadata) {
-        if (canvasMetadata) {
-            this.pipelineCanvasMetadata = canvasMetadata;
-            this.pipelineCanvasMetadataAvailable = true;
-        } else {
-            this.pipelineCanvasMetadataAvailable = false;
-            this.pipelineCanvasMetadata = new PipelineCanvasMetadata();
-        }
-    }
-
-    displayPipelineInEditor(
-        autoLayout: boolean,
-        pipelineCanvasMetadata?: PipelineCanvasMetadata,
-    ): void {
-        setTimeout(() => {
-            this.pipelinePositioningService.displayPipeline(
-                this.rawPipelineModel,
-                '#assembly',
-                false,
-                autoLayout,
-                pipelineCanvasMetadata,
-            );
-            this.editorService.makePipelineAssemblyEmpty(false);
-            this.ngZone.run(() => {
-                this.pipelineValid =
-                    this.pipelineValidationService.isValidPipeline(
-                        this.rawPipelineModel.filter(
-                            pe => !pe.settings.disabled,
-                        ),
-                        false,
-                    );
-            });
-            this.pipelineComponent.triggerPipelineModification();
-        });
-    }
-
-    isPipelineAssemblyEmpty() {
-        return (
-            this.rawPipelineModel.length === 0 ||
-            this.rawPipelineModel.every(pe => pe.settings.disabled)
-        );
-    }
-
-    panLeft() {
-        this.pan(100, 0);
-    }
-
-    panRight() {
-        this.pan(-100, 0);
-    }
-
-    panUp() {
-        this.pan(0, 100);
-    }
-
-    panDown() {
-        this.pan(0, -100);
-    }
-
-    panHome() {
-        this.panAbsolute(0, 0);
-    }
-
-    pan(xOffset: number, yOffset: number) {
-        const currentPan = this.panzoom.getPan();
-        const panX = Math.min(0, currentPan.x + xOffset);
-        const panY = Math.min(0, currentPan.y + yOffset);
-        this.panzoom.pan(panX, panY);
-    }
-
-    panAbsolute(x: number, y: number) {
-        this.panzoom.pan(x, y);
-    }
-
-    triggerPipelinePreview() {
-        this.pipelineComponent.initiatePipelineElementPreview();
-    }
-
-    openDiscoverDialog() {
-        this.dialogService.open(PipelineElementDiscoveryComponent, {
-            panelType: PanelType.SLIDE_IN_PANEL,
-            title: 'Discover pipeline elements',
-            width: '50vw',
-            data: {
-                currentElements: this.allElements,
-                rawPipelineModel: this.rawPipelineModel,
-            },
-        });
+    togglePreview(): void {
+        this.previewModeActive = !this.previewModeActive;
+        this.drawingAreaComponent.initiatePipelineElementPreview();
     }
 
-    ngOnDestroy() {
-        this.moveSub?.unsubscribe();
+    triggerCacheUpdate(): void {
+        this.assemblyOptionsComponent.triggerCacheUpdate();
     }
 }
diff --git 
a/ui/src/app/editor/components/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.html
 
b/ui/src/app/editor/components/pipeline-element-icon-stand/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.html
similarity index 100%
rename from 
ui/src/app/editor/components/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.html
rename to 
ui/src/app/editor/components/pipeline-element-icon-stand/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.html
diff --git 
a/ui/src/app/editor/components/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.scss
 
b/ui/src/app/editor/components/pipeline-element-icon-stand/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.scss
similarity index 100%
rename from 
ui/src/app/editor/components/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.scss
rename to 
ui/src/app/editor/components/pipeline-element-icon-stand/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.scss
diff --git 
a/ui/src/app/editor/components/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.ts
 
b/ui/src/app/editor/components/pipeline-element-icon-stand/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.ts
similarity index 91%
rename from 
ui/src/app/editor/components/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.ts
rename to 
ui/src/app/editor/components/pipeline-element-icon-stand/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.ts
index ab0e2385c4..2fa186a3a7 100644
--- 
a/ui/src/app/editor/components/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.ts
+++ 
b/ui/src/app/editor/components/pipeline-element-icon-stand/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.ts
@@ -20,9 +20,9 @@ import { Component, Input, OnInit } from '@angular/core';
 import {
     PipelineElementType,
     PipelineElementUnion,
-} from '../../model/editor.model';
-import { PipelineElementTypeUtils } from '../../utils/editor.utils';
-import { EditorService } from '../../services/editor.service';
+} from '../../../model/editor.model';
+import { PipelineElementTypeUtils } from '../../../utils/editor.utils';
+import { EditorService } from '../../../services/editor.service';
 
 @Component({
     selector: 'sp-pe-icon-stand-row',
diff --git 
a/ui/src/app/editor/components/pipeline-element-icon-stand/pipeline-element-icon-stand.component.ts
 
b/ui/src/app/editor/components/pipeline-element-icon-stand/pipeline-element-icon-stand.component.ts
index 88136e51d3..9e1c4e2340 100644
--- 
a/ui/src/app/editor/components/pipeline-element-icon-stand/pipeline-element-icon-stand.component.ts
+++ 
b/ui/src/app/editor/components/pipeline-element-icon-stand/pipeline-element-icon-stand.component.ts
@@ -17,7 +17,6 @@
  */
 
 import { AfterViewInit, Component, Input, OnInit } from '@angular/core';
-import { RestApi } from '../../../services/rest-api.service';
 import {
     PeCategory,
     PipelineElementType,
@@ -72,7 +71,6 @@ export class PipelineElementIconStandComponent
     };
 
     constructor(
-        private restApi: RestApi,
         private editorService: EditorService,
         private router: Router,
     ) {}
diff --git 
a/ui/src/app/editor/components/pipeline/dropped-pipeline-element/dropped-pipeline-element.component.html
 
b/ui/src/app/editor/components/pipeline/dropped-pipeline-element/dropped-pipeline-element.component.html
new file mode 100644
index 0000000000..6004bdcb46
--- /dev/null
+++ 
b/ui/src/app/editor/components/pipeline/dropped-pipeline-element/dropped-pipeline-element.component.html
@@ -0,0 +1,103 @@
+<!--
+  ~ 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.
+  ~
+  -->
+<div style="z-index: 5" [ngClass]="elementCssClasses">
+    <div
+        class="pipeline-element-progress-container sp-fade"
+        *ngIf="pipelineElementConfig.settings.loadingStatus"
+    >
+        <mat-spinner
+            [mode]="'indeterminate'"
+            class="pipeline-element-progress"
+            [diameter]="40"
+            color="accent"
+        ></mat-spinner>
+    </div>
+    <div
+        class="pipeline-element-loading-container sp-fade-opacity"
+        *ngIf="pipelineElementConfig.settings.loadingStatus"
+    ></div>
+    <div
+        class="pipeline-element-configuration-status {{
+            pipelineElementConfig.type === 'stream'
+                ? 'pi-stream'
+                : 'pi-processor'
+        }}"
+        *ngIf="!readonly"
+    >
+        <i
+            class="material-icons pipeline-element-configuration-invalid-icon"
+            *ngIf="pipelineElementConfig.settings.completed === 3"
+        >
+            warning
+        </i>
+        <i
+            class="material-icons pipeline-element-configuration-modified-icon"
+            *ngIf="pipelineElementConfig.settings.completed === 2"
+        >
+            warning
+        </i>
+        <i
+            class="material-icons pipeline-element-configuration-ok-icon"
+            *ngIf="pipelineElementConfig.settings.completed === 1"
+        >
+            check_circle
+        </i>
+    </div>
+    <sp-pipeline-element
+        [pipelineElement]="pipelineElementConfig.payload"
+    ></sp-pipeline-element>
+</div>
+<sp-pipeline-element-statistics
+    *ngIf="metricsInfo"
+    [pipelineElement]="pipelineElementConfig.payload"
+    [metricsInfo]="metricsInfo[pipelineElementConfig.payload.elementId]"
+>
+</sp-pipeline-element-statistics>
+<sp-pipeline-element-options
+    *ngIf="!readonly"
+    (delete)="deleteEmitter.emit($event)"
+    (customize)="showCustomizeEmitter.emit($event)"
+    [currentMouseOverElement]="currentMouseOverElement"
+    [pipelineValid]="pipelineValid"
+    [allElements]="allElements"
+    [pipelineElement]="pipelineElementConfig"
+    [rawPipelineModel]="rawPipelineModel"
+    [pipelineElementId]="
+        pipelineElementConfig.type === 'stream' ||
+        pipelineElementConfig.type === 'set'
+            ? pipelineElementConfig.payload.elementId
+            : pipelineElementConfig.payload.belongsTo
+    "
+    [internalId]="pipelineElementConfig.payload.dom"
+    [attr.data-cy]="
+        'sp-pe-menu-' +
+        pipelineElementConfig.payload.name.toLowerCase().replaceAll(' ', '_')
+    "
+>
+</sp-pipeline-element-options>
+<sp-pipeline-element-preview
+    *ngIf="
+        previewModeActive &&
+        pipelinePreview.supportedPipelineElementDomIds.indexOf(
+            pipelineElementConfig.payload.elementId
+        ) > -1
+    "
+    [previewId]="pipelinePreview.previewId"
+    [elementId]="pipelineElementConfig.payload.elementId"
+>
+</sp-pipeline-element-preview>
diff --git 
a/ui/src/app/editor/components/pipeline/dropped-pipeline-element/dropped-pipeline-element.component.ts
 
b/ui/src/app/editor/components/pipeline/dropped-pipeline-element/dropped-pipeline-element.component.ts
new file mode 100644
index 0000000000..7c079c8408
--- /dev/null
+++ 
b/ui/src/app/editor/components/pipeline/dropped-pipeline-element/dropped-pipeline-element.component.ts
@@ -0,0 +1,79 @@
+/*
+ * 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 { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import {
+    PipelineElementConfig,
+    PipelineElementUnion,
+} from '../../../model/editor.model';
+import {
+    PipelinePreviewModel,
+    SpMetricsEntry,
+} from '@streampipes/platform-services';
+
+@Component({
+    selector: 'sp-dropped-pipeline-element',
+    templateUrl: './dropped-pipeline-element.component.html',
+})
+export class DroppedPipelineElementComponent implements OnInit {
+    @Input()
+    pipelineElementConfig: PipelineElementConfig;
+
+    @Input()
+    readonly: boolean;
+
+    @Input()
+    allElements: PipelineElementUnion[];
+
+    @Input()
+    metricsInfo: Record<string, SpMetricsEntry>;
+
+    @Input()
+    previewModeActive: boolean;
+
+    @Input()
+    pipelinePreview: PipelinePreviewModel;
+
+    @Input()
+    rawPipelineModel: PipelineElementConfig[];
+
+    @Input()
+    currentMouseOverElement = '';
+
+    @Input()
+    pipelineValid: boolean;
+
+    @Output()
+    deleteEmitter: EventEmitter<PipelineElementConfig> = new EventEmitter();
+
+    @Output()
+    showCustomizeEmitter: EventEmitter<PipelineElementConfig> =
+        new EventEmitter();
+
+    elementCssClasses: string;
+
+    ngOnInit() {
+        this.applyCssClasses();
+    }
+
+    applyCssClasses(): void {
+        this.elementCssClasses = `${this.pipelineElementConfig.type} 
+    ${this.pipelineElementConfig.settings.connectable} 
+    ${this.pipelineElementConfig.settings.displaySettings}`;
+    }
+}
diff --git a/ui/src/app/editor/components/pipeline/pipeline.component.html 
b/ui/src/app/editor/components/pipeline/pipeline.component.html
index 53039821f5..20b984a570 100644
--- a/ui/src/app/editor/components/pipeline/pipeline.component.html
+++ b/ui/src/app/editor/components/pipeline/pipeline.component.html
@@ -16,96 +16,29 @@
   ~
   -->
 <div
-    [attr.data-jtk-managed]="pipelineElement.payload.dom"
-    id="{{ pipelineElement.payload.dom }}"
-    style="{{ getElementCss(pipelineElement.settings) }}"
-    (click)="updateOptionsClick(pipelineElement.payload.dom)"
-    (mouseenter)="updateMouseover(pipelineElement.payload.dom)"
-    (mouseleave)="updateMouseover('')"
-    *ngFor="let pipelineElement of rawPipelineModel | enabledPipelineElement"
+    [attr.data-jtk-managed]="pipelineElementConfig.payload.dom"
+    id="{{ pipelineElementConfig.payload.dom }}"
     class="sp-no-pan"
+    [ngStyle]="getCssStyle(pipelineElementConfig)"
+    (click)="updateOptionsClick(pipelineElementConfig.payload.dom)"
+    (mouseenter)="updateMouseover(pipelineElementConfig.payload.dom)"
+    (mouseleave)="updateMouseover('')"
+    *ngFor="
+        let pipelineElementConfig of rawPipelineModel | enabledPipelineElement
+    "
 >
-    <div style="z-index: 5" [ngClass]="getElementCssClasses(pipelineElement)">
-        <div
-            class="pipeline-element-progress-container sp-fade"
-            *ngIf="pipelineElement.settings.loadingStatus"
-        >
-            <mat-spinner
-                [mode]="'indeterminate'"
-                class="pipeline-element-progress"
-                [diameter]="40"
-                color="accent"
-            ></mat-spinner>
-        </div>
-        <div
-            class="pipeline-element-loading-container sp-fade-opacity"
-            *ngIf="pipelineElement.settings.loadingStatus"
-        ></div>
-        <div
-            class="pipeline-element-configuration-status {{
-                pipelineElement.type === 'stream' ? 'pi-stream' : 
'pi-processor'
-            }}"
-            *ngIf="!preview"
-        >
-            <i
-                class="material-icons 
pipeline-element-configuration-invalid-icon"
-                *ngIf="pipelineElement.settings.completed === 3"
-            >
-                warning
-            </i>
-            <i
-                class="material-icons 
pipeline-element-configuration-modified-icon"
-                *ngIf="pipelineElement.settings.completed === 2"
-            >
-                warning
-            </i>
-            <i
-                class="material-icons pipeline-element-configuration-ok-icon"
-                *ngIf="pipelineElement.settings.completed === 1"
-            >
-                check_circle
-            </i>
-        </div>
-        <sp-pipeline-element
-            [pipelineElement]="pipelineElement.payload"
-        ></sp-pipeline-element>
-    </div>
-    <sp-pipeline-element-statistics
-        *ngIf="metricsInfo"
-        [pipelineElement]="pipelineElement.payload"
-        [metricsInfo]="metricsInfo[pipelineElement.payload.elementId]"
-    >
-    </sp-pipeline-element-statistics>
-    <sp-pipeline-element-options
-        *ngIf="!preview"
-        (delete)="handleDeleteOption($event)"
-        (customize)="showCustomizeDialog($event)"
+    <sp-dropped-pipeline-element
+        [allElements]="allElements"
         [currentMouseOverElement]="currentMouseOverElement"
+        [metricsInfo]="metricsInfo"
+        [pipelineElementConfig]="pipelineElementConfig"
+        [previewModeActive]="previewModeActive"
+        [pipelinePreview]="pipelinePreview"
         [pipelineValid]="pipelineValid"
-        [allElements]="allElements"
-        [pipelineElement]="pipelineElement"
         [rawPipelineModel]="rawPipelineModel"
-        [pipelineElementId]="
-            pipelineElement.type === 'stream' || pipelineElement.type === 'set'
-                ? pipelineElement.payload.elementId
-                : pipelineElement.payload.belongsTo
-        "
-        [internalId]="pipelineElement.payload.dom"
-        [attr.data-cy]="
-            'sp-pe-menu-' +
-            pipelineElement.payload.name.toLowerCase().replaceAll(' ', '_')
-        "
-    >
-    </sp-pipeline-element-options>
-    <sp-pipeline-element-preview
-        *ngIf="
-            previewModeActive &&
-            pipelinePreview.supportedPipelineElementDomIds.indexOf(
-                pipelineElement.payload.elementId
-            ) > -1
-        "
-        [previewId]="pipelinePreview.previewId"
-        [elementId]="pipelineElement.payload.elementId"
+        [readonly]="readonly"
+        (showCustomizeEmitter)="showCustomizeDialog($event)"
+        (deleteEmitter)="handleDeleteOption($event)"
     >
-    </sp-pipeline-element-preview>
+    </sp-dropped-pipeline-element>
 </div>
diff --git a/ui/src/app/editor/components/pipeline/pipeline.component.ts 
b/ui/src/app/editor/components/pipeline/pipeline.component.ts
index 0dcb022857..1cfdd1c2e0 100644
--- a/ui/src/app/editor/components/pipeline/pipeline.component.ts
+++ b/ui/src/app/editor/components/pipeline/pipeline.component.ts
@@ -43,7 +43,6 @@ import {
     DataSinkInvocation,
     Notification,
     Pipeline,
-    PipelineCanvasMetadata,
     PipelineEdgeValidation,
     PipelineModificationMessage,
     PipelinePreviewModel,
@@ -60,9 +59,7 @@ import {
 import { EditorService } from '../../services/editor.service';
 import { MatchingErrorComponent } from 
'../../dialog/matching-error/matching-error.component';
 import { MatDialog } from '@angular/material/dialog';
-import { forkJoin, Subscription } from 'rxjs';
 import { JsplumbFactoryService } from '../../services/jsplumb-factory.service';
-import { PipelinePositioningService } from 
'../../services/pipeline-positioning.service';
 import {
     EVENT_CONNECTION,
     EVENT_CONNECTION_ABORT,
@@ -72,21 +69,15 @@ import {
 } from '@jsplumb/browser-ui';
 import { PipelineStyleService } from '../../services/pipeline-style.service';
 import { IdGeneratorService } from 
'../../../core-services/id-generator/id-generator.service';
-import { LivePreviewService } from '../../../services/live-preview.service';
-import { HttpDownloadProgressEvent } from '@angular/common/http';
 
 @Component({
     selector: 'sp-pipeline',
     templateUrl: './pipeline.component.html',
-    styleUrls: ['./pipeline.component.scss'],
 })
 export class PipelineComponent implements OnInit, OnDestroy {
     @Input()
     pipelineValid: boolean;
 
-    @Input()
-    canvasId: string;
-
     @Input()
     rawPipelineModel: PipelineElementConfig[];
 
@@ -94,44 +85,35 @@ export class PipelineComponent implements OnInit, OnDestroy 
{
     allElements: PipelineElementUnion[];
 
     @Input()
-    preview: boolean;
+    readonly: boolean;
 
     @Input()
-    pipelineCached: boolean;
+    metricsInfo: Record<string, SpMetricsEntry>;
 
     @Output()
-    pipelineCachedChanged: EventEmitter<boolean> = new EventEmitter<boolean>();
-
-    @Input()
-    pipelineCacheRunning: boolean;
-
-    @Input()
-    pipelineCanvasMetadata: PipelineCanvasMetadata;
-
-    @Input()
-    metricsInfo: Record<string, SpMetricsEntry>;
+    deletePreviewEmitter: EventEmitter<boolean> = new EventEmitter<boolean>();
 
     @Output()
-    pipelineCacheRunningChanged: EventEmitter<boolean> =
-        new EventEmitter<boolean>();
+    triggerPipelineCacheUpdateEmitter: EventEmitter<void> = new EventEmitter();
 
-    currentMouseOverElement: string;
+    currentMouseOverElement = '';
     currentPipelineModel: Pipeline;
     idCounter: any;
     currentZoomLevel: any;
 
     JsplumbBridge: JsplumbBridge;
 
+    @Input()
     previewModeActive = false;
+
+    @Input()
     pipelinePreview: PipelinePreviewModel;
-    pipelinePreviewSub: Subscription;
 
     shouldOpenCustomizeSettings = false;
 
     constructor(
         private jsplumbService: JsplumbService,
         private pipelineEditorService: PipelineEditorService,
-        private pipelinePositioningService: PipelinePositioningService,
         private jsplumbFactoryService: JsplumbFactoryService,
         private objectProvider: ObjectProvider,
         private editorService: EditorService,
@@ -142,9 +124,7 @@ export class PipelineComponent implements OnInit, OnDestroy 
{
         private dialogService: DialogService,
         private dialog: MatDialog,
         private ngZone: NgZone,
-        private livePreviewService: LivePreviewService,
     ) {
-        this.currentMouseOverElement = '';
         this.currentPipelineModel = new Pipeline();
         this.idCounter = 0;
 
@@ -153,10 +133,24 @@ export class PipelineComponent implements OnInit, 
OnDestroy {
 
     ngOnInit() {
         this.JsplumbBridge = this.jsplumbFactoryService.getJsplumbBridge(
-            this.preview,
+            this.readonly,
         );
-        this.initAssembly();
-        this.initPlumb();
+        if (!this.readonly) {
+            this.initAssembly();
+            this.initPlumb();
+        }
+    }
+
+    getCssStyle(
+        pipelineElementConfig: PipelineElementConfig,
+    ): Record<string, string> {
+        return {
+            position: 'absolute',
+            width: '90px',
+            height: '90px',
+            left: pipelineElementConfig.settings.position.x + 'px',
+            top: pipelineElementConfig.settings.position.y + 'px',
+        };
     }
 
     validatePipeline(pm?: PipelineModificationMessage) {
@@ -167,7 +161,7 @@ export class PipelineComponent implements OnInit, OnDestroy 
{
                         this.rawPipelineModel.filter(
                             pe => !pe.settings.disabled,
                         ),
-                        this.preview,
+                        this.readonly,
                         pm,
                     );
             });
@@ -175,8 +169,8 @@ export class PipelineComponent implements OnInit, OnDestroy 
{
     }
 
     ngOnDestroy() {
-        this.deletePipelineElementPreview(false);
-        this.jsplumbFactoryService.destroy(this.preview);
+        this.deletePreviewEmitter.emit(false);
+        this.jsplumbFactoryService.destroy();
     }
 
     @HostListener('window:beforeunload')
@@ -193,44 +187,6 @@ export class PipelineComponent implements OnInit, 
OnDestroy {
             this.currentMouseOverElement === elementId ? '' : elementId;
     }
 
-    getElementCss(currentPipelineElementSettings) {
-        return (
-            'position:absolute;' +
-            (this.preview ? 'width:90px;' : 'width:90px;') +
-            (this.preview ? 'height:90px;' : 'height:90px;') +
-            'left: ' +
-            currentPipelineElementSettings.position.x +
-            'px; ' +
-            'top: ' +
-            currentPipelineElementSettings.position.y +
-            'px; '
-        );
-    }
-
-    getElementCssClasses(currentPipelineElement: PipelineElementConfig) {
-        return (
-            currentPipelineElement.type +
-            ' ' +
-            currentPipelineElement.settings.connectable +
-            ' ' +
-            currentPipelineElement.settings.displaySettings
-        );
-    }
-
-    isStreamInPipeline() {
-        return this.isInPipeline('stream');
-    }
-
-    isSetInPipeline() {
-        return this.isInPipeline('set');
-    }
-
-    isInPipeline(type: string) {
-        return this.rawPipelineModel.some(
-            x => x.type === type && !x.settings.disabled,
-        );
-    }
-
     findPipelineElementByElementId(elementId: string) {
         return this.allElements.find(a => a.elementId === elementId);
     }
@@ -291,7 +247,7 @@ export class PipelineComponent implements OnInit, OnDestroy 
{
                 }
                 this.JsplumbBridge.repaintEverything();
                 this.validatePipeline();
-                this.triggerPipelineCacheUpdate();
+                this.triggerPipelineCacheUpdateEmitter.emit();
             },
         });
     }
@@ -326,7 +282,7 @@ export class PipelineComponent implements OnInit, OnDestroy 
{
         }
         this.JsplumbBridge.repaintEverything();
         this.validatePipeline();
-        this.triggerPipelineCacheUpdate();
+        this.triggerPipelineCacheUpdateEmitter.emit();
     }
 
     initPlumb() {
@@ -418,7 +374,7 @@ export class PipelineComponent implements OnInit, OnDestroy 
{
                                 if (
                                     this.jsplumbService.isFullyConnected(
                                         pe,
-                                        this.preview,
+                                        this.readonly,
                                     )
                                 ) {
                                     const payload =
@@ -439,7 +395,7 @@ export class PipelineComponent implements OnInit, OnDestroy 
{
                                             
PipelineElementConfigurationStatus.OK,
                                         );
                                         this.announceConfiguredElement(pe);
-                                        this.triggerPipelineCacheUpdate();
+                                        
this.triggerPipelineCacheUpdateEmitter.emit();
                                     }
                                 }
                             } else {
@@ -552,34 +508,6 @@ export class PipelineComponent implements OnInit, 
OnDestroy {
         return custom;
     }
 
-    triggerPipelineCacheUpdate() {
-        setTimeout(() => {
-            this.pipelineCacheRunning = true;
-            this.pipelineCacheRunningChanged.emit(this.pipelineCacheRunning);
-            this.pipelinePositioningService.collectPipelineElementPositions(
-                this.pipelineCanvasMetadata,
-                this.rawPipelineModel,
-            );
-            const updateCachedPipeline =
-                this.editorService.updateCachedPipeline(this.rawPipelineModel);
-            const updateCachedCanvasMetadata =
-                this.editorService.updateCachedCanvasMetadata(
-                    this.pipelineCanvasMetadata,
-                );
-            forkJoin([
-                updateCachedPipeline,
-                updateCachedCanvasMetadata,
-            ]).subscribe(() => {
-                this.pipelineCacheRunning = false;
-                this.pipelineCacheRunningChanged.emit(
-                    this.pipelineCacheRunning,
-                );
-                this.pipelineCached = true;
-                this.pipelineCachedChanged.emit(this.pipelineCached);
-            });
-        });
-    }
-
     showErrorDialog(title: string, description: string) {
         this.dialog.open(ConfirmDialogComponent, {
             width: '500px',
@@ -624,10 +552,10 @@ export class PipelineComponent implements OnInit, 
OnDestroy {
                     .updatePipeline(this.currentPipelineModel)
                     .subscribe(pm => {
                         this.modifyPipeline(pm);
-                        this.triggerPipelineCacheUpdate();
+                        this.triggerPipelineCacheUpdateEmitter.emit();
                         this.announceConfiguredElement(pipelineElementConfig);
                         if (this.previewModeActive) {
-                            this.deletePipelineElementPreview(true);
+                            this.deletePreviewEmitter.emit(true);
                         }
                         this.validatePipeline(pm);
                     });
@@ -641,44 +569,6 @@ export class PipelineComponent implements OnInit, 
OnDestroy {
         this.editorService.announceConfiguredElement(pe.payload.dom);
     }
 
-    initiatePipelineElementPreview() {
-        if (!this.previewModeActive) {
-            const pipeline = this.objectProvider.makePipeline(
-                this.rawPipelineModel,
-            );
-            this.editorService
-                .initiatePipelinePreview(pipeline)
-                .subscribe(response => {
-                    this.pipelinePreview = response;
-                    this.previewModeActive = true;
-                    this.pipelinePreviewSub = this.editorService
-                        .getPipelinePreviewResult(response.previewId)
-                        .subscribe(res => {
-                            const data = this.livePreviewService.convert(
-                                res as HttpDownloadProgressEvent,
-                            );
-                            this.livePreviewService.eventSub.next(data);
-                        });
-                });
-        } else {
-            this.deletePipelineElementPreview(false);
-        }
-    }
-
-    deletePipelineElementPreview(resume: boolean) {
-        if (this.previewModeActive) {
-            this.pipelinePreviewSub?.unsubscribe();
-            this.editorService
-                .deletePipelinePreviewRequest(this.pipelinePreview.previewId)
-                .subscribe(() => {
-                    this.previewModeActive = false;
-                    if (resume) {
-                        this.initiatePipelineElementPreview();
-                    }
-                });
-        }
-    }
-
     triggerPipelineModification() {
         this.currentPipelineModel = this.objectProvider.makePipeline(
             this.rawPipelineModel,
@@ -690,7 +580,7 @@ export class PipelineComponent implements OnInit, OnDestroy 
{
                 this.pipelineValid =
                     this.pipelineValidationService.isValidPipeline(
                         this.rawPipelineModel,
-                        this.preview,
+                        this.readonly,
                         pm,
                     );
             });
diff --git a/ui/src/app/editor/editor.component.html 
b/ui/src/app/editor/editor.component.html
index 27da459f4a..b334d81895 100644
--- a/ui/src/app/editor/editor.component.html
+++ b/ui/src/app/editor/editor.component.html
@@ -40,7 +40,7 @@
             <div
                 id="shepherd-test"
                 style="
-                    padding: 0px;
+                    padding: 0;
                     border-bottom: 1px solid #ffffff;
                     margin-right: 5px;
                 "
@@ -55,9 +55,12 @@
         <sp-pipeline-assembly
             fxFlex="100"
             style="margin-left: 10px"
+            *ngIf="allElementsLoaded && allMetadataLoaded"
             [rawPipelineModel]="rawPipelineModel"
             [allElements]="allElements"
-            [currentModifiedPipelineId]="currentModifiedPipelineId"
+            [originalPipeline]="originalPipeline"
+            [pipelineCanvasMetadata]="pipelineCanvasMetadata"
+            [pipelineCanvasMetadataAvailable]="pipelineCanvasMetadataAvailable"
         >
         </sp-pipeline-assembly>
     </div>
diff --git a/ui/src/app/editor/editor.component.ts 
b/ui/src/app/editor/editor.component.ts
index a6ed4a5a0b..e33fd34c47 100644
--- a/ui/src/app/editor/editor.component.ts
+++ b/ui/src/app/editor/editor.component.ts
@@ -17,15 +17,24 @@
  */
 
 import { Component, OnInit } from '@angular/core';
-import { PipelineElementService } from '@streampipes/platform-services';
+import {
+    Pipeline,
+    PipelineCanvasMetadata,
+    PipelineCanvasMetadataService,
+    PipelineElementService,
+    PipelineService,
+} from '@streampipes/platform-services';
 import {
     PipelineElementConfig,
     PipelineElementUnion,
 } from './model/editor.model';
 import { SpBreadcrumbService } from '@streampipes/shared-ui';
 import { ActivatedRoute } from '@angular/router';
-import { zip } from 'rxjs';
+import { forkJoin, of, zip } from 'rxjs';
 import { SpPipelineRoutes } from '../pipelines/pipelines.routes';
+import { catchError } from 'rxjs/operators';
+import { EditorService } from './services/editor.service';
+import { JsplumbService } from './services/jsplumb.service';
 
 @Component({
     selector: 'sp-editor',
@@ -36,21 +45,29 @@ export class EditorComponent implements OnInit {
     allElements: PipelineElementUnion[] = [];
 
     rawPipelineModel: PipelineElementConfig[] = [];
-    currentModifiedPipelineId: string;
+    originalPipeline: Pipeline;
 
     allElementsLoaded = false;
+    allMetadataLoaded = false;
+    pipelineCanvasMetadata: PipelineCanvasMetadata;
+    pipelineCanvasMetadataAvailable: boolean;
 
     constructor(
         private pipelineElementService: PipelineElementService,
         private activatedRoute: ActivatedRoute,
         private breadcrumbService: SpBreadcrumbService,
+        private editorService: EditorService,
+        private pipelineService: PipelineService,
+        private jsplumbService: JsplumbService,
+        private pipelineCanvasMetadataService: PipelineCanvasMetadataService,
     ) {}
 
     ngOnInit() {
         const pipelineId = this.activatedRoute.snapshot.params.pipelineId;
         if (pipelineId) {
-            this.currentModifiedPipelineId = pipelineId;
+            this.loadPipelineToModify(pipelineId);
         } else {
+            this.loadCachedPipeline();
             this.breadcrumbService.updateBreadcrumb([
                 SpPipelineRoutes.BASE,
                 { label: 'New Pipeline' },
@@ -71,4 +88,61 @@ export class EditorComponent implements OnInit {
             this.allElementsLoaded = true;
         });
     }
+
+    loadCachedPipeline() {
+        const cachedPipeline = this.editorService.getCachedPipeline();
+        const cachedCanvasMetadata =
+            this.editorService.getCachedPipelineCanvasMetadata();
+        forkJoin([cachedPipeline, cachedCanvasMetadata]).subscribe(results => {
+            if (results[0] && results[0].length > 0) {
+                this.rawPipelineModel = results[0] as PipelineElementConfig[];
+                this.handleCanvasMetadataResponse(results[1]);
+            } else {
+                this.pipelineCanvasMetadata = new PipelineCanvasMetadata();
+                this.pipelineCanvasMetadataAvailable = false;
+            }
+            this.allMetadataLoaded = true;
+        });
+    }
+
+    loadPipelineToModify(pipelineId: string) {
+        const pipelineReq = this.pipelineService.getPipelineById(pipelineId);
+        const canvasMetadataReq = this.pipelineCanvasMetadataService
+            .getPipelineCanvasMetadata(pipelineId)
+            .pipe(
+                catchError(() => {
+                    this.handleCanvasMetadataResponse(undefined);
+                    return of(undefined);
+                }),
+            );
+
+        forkJoin([pipelineReq, canvasMetadataReq]).subscribe(
+            ([pipelineResp, canvasResp]) => {
+                if (pipelineResp) {
+                    this.originalPipeline = pipelineResp;
+                    this.breadcrumbService.updateBreadcrumb([
+                        SpPipelineRoutes.BASE,
+                        { label: this.originalPipeline.name },
+                        { label: 'Modify' },
+                    ]);
+                    this.rawPipelineModel = 
this.jsplumbService.makeRawPipeline(
+                        this.originalPipeline,
+                        false,
+                    );
+                }
+                this.handleCanvasMetadataResponse(canvasResp);
+                this.allMetadataLoaded = true;
+            },
+        );
+    }
+
+    handleCanvasMetadataResponse(canvasMetadata: PipelineCanvasMetadata) {
+        if (canvasMetadata !== undefined) {
+            this.pipelineCanvasMetadata = canvasMetadata;
+            this.pipelineCanvasMetadataAvailable = true;
+        } else {
+            this.pipelineCanvasMetadataAvailable = false;
+            this.pipelineCanvasMetadata = new PipelineCanvasMetadata();
+        }
+    }
 }
diff --git a/ui/src/app/editor/editor.module.ts 
b/ui/src/app/editor/editor.module.ts
index 32a6f823c6..8922b4ff6d 100644
--- a/ui/src/app/editor/editor.module.ts
+++ b/ui/src/app/editor/editor.module.ts
@@ -46,7 +46,7 @@ import { EnabledPipelineElementFilter } from 
'./filter/enabled-pipeline-element.
 import { PipelineElementPreviewComponent } from 
'./components/pipeline-element-preview/pipeline-element-preview.component';
 import { PipelineElementDiscoveryComponent } from 
'./dialog/pipeline-element-discovery/pipeline-element-discovery.component';
 import { PlatformServicesModule } from '@streampipes/platform-services';
-import { PipelineElementIconStandRowComponent } from 
'./components/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component';
+import { PipelineElementIconStandRowComponent } from 
'./components/pipeline-element-icon-stand/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component';
 import { PipelineElementTypeFilterPipe } from 
'./services/pipeline-element-type-filter.pipe';
 import { PipelineElementNameFilterPipe } from 
'./services/pipeline-element-name-filter.pipe';
 import { PipelineElementGroupFilterPipe } from 
'./services/pipeline-element-group-filter.pipe';
@@ -77,6 +77,11 @@ import { MatSliderModule } from '@angular/material/slider';
 import { PipelineElementStatisticsComponent } from 
'./components/pipeline-element-statistics/pipeline-element-statistics.component';
 import { PipelineElementStatisticsBadgeComponent } from 
'./components/pipeline-element-statistics/pipeline-element-statistics-badge/pipeline-element-statistics-badge.component';
 import { SavePipelineSettingsComponent } from 
'./dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component';
+import { PipelineAssemblyOptionsComponent } from 
'./components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component';
+import { PipelineAssemblyOptionsPipelineCacheComponent } from 
'./components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options-pipeline-cache/pipeline-assembly-options-pipeline-cache.component';
+import { PipelineAssemblyDrawingAreaPanZoomComponent } from 
'./components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area-pan-zoom/pipeline-assembly-drawing-area-pan-zoom.component';
+import { PipelineAssemblyDrawingAreaComponent } from 
'./components/pipeline-assembly/pipeline-assembly-drawing-area/pipeline-assembly-drawing-area.component';
+import { DroppedPipelineElementComponent } from 
'./components/pipeline/dropped-pipeline-element/dropped-pipeline-element.component';
 
 @NgModule({
     imports: [
@@ -121,6 +126,7 @@ import { SavePipelineSettingsComponent } from 
'./dialog/save-pipeline/save-pipel
         CompatibleElementsComponent,
         CustomizeComponent,
         CustomOutputStrategyComponent,
+        DroppedPipelineElementComponent,
         EditorComponent,
         EnabledPipelineElementFilter,
         MatchingErrorComponent,
@@ -128,6 +134,10 @@ import { SavePipelineSettingsComponent } from 
'./dialog/save-pipeline/save-pipel
         OutputStrategyComponent,
         UserDefinedOutputStrategyComponent,
         PipelineAssemblyComponent,
+        PipelineAssemblyDrawingAreaComponent,
+        PipelineAssemblyDrawingAreaPanZoomComponent,
+        PipelineAssemblyOptionsComponent,
+        PipelineAssemblyOptionsPipelineCacheComponent,
         PipelineElementComponent,
         PipelineElementDiscoveryComponent,
         PipelineElementIconStandComponent,
@@ -147,7 +157,12 @@ import { SavePipelineSettingsComponent } from 
'./dialog/save-pipeline/save-pipel
         SafeCss,
     ],
     providers: [SafeCss],
-    exports: [EditorComponent, PipelineComponent, PipelineElementComponent],
+    exports: [
+        EditorComponent,
+        PipelineComponent,
+        PipelineElementComponent,
+        PipelineAssemblyDrawingAreaComponent,
+    ],
 })
 export class EditorModule {
     constructor() {}
diff --git a/ui/src/app/editor/services/jsplumb-factory.service.ts 
b/ui/src/app/editor/services/jsplumb-factory.service.ts
index 679b61d674..9d554cae82 100644
--- a/ui/src/app/editor/services/jsplumb-factory.service.ts
+++ b/ui/src/app/editor/services/jsplumb-factory.service.ts
@@ -30,10 +30,7 @@ import { JsplumbConfigService } from 
'./jsplumb-config.service';
 @Injectable({ providedIn: 'root' })
 export class JsplumbFactoryService {
     pipelineEditorInstance: BrowserJsPlumbInstance;
-    pipelinePreviewInstance: BrowserJsPlumbInstance;
-
     pipelineEditorBridge: JsplumbBridge;
-    pipelinePreviewBridge: JsplumbBridge;
 
     constructor(
         private pipelineElementDraggedService: PipelineElementDraggedService,
@@ -41,26 +38,16 @@ export class JsplumbFactoryService {
     ) {}
 
     getJsplumbBridge(previewConfig: boolean): JsplumbBridge {
-        if (!previewConfig) {
-            if (!this.pipelineEditorBridge) {
-                this.pipelineEditorInstance = 
this.makePipelineEditorInstance();
-                this.prepareJsplumb(this.pipelineEditorInstance);
-                this.pipelineEditorBridge = new JsplumbBridge(
-                    this.pipelineEditorInstance,
-                );
-            }
-            return this.pipelineEditorBridge;
-        } else {
-            if (!this.pipelinePreviewBridge) {
-                this.pipelinePreviewInstance =
-                    this.makePipelinePreviewInstance();
-                this.prepareJsplumb(this.pipelinePreviewInstance);
-                this.pipelinePreviewBridge = new JsplumbBridge(
-                    this.pipelinePreviewInstance,
-                );
-            }
-            return this.pipelinePreviewBridge;
+        if (!this.pipelineEditorBridge) {
+            this.pipelineEditorInstance = previewConfig
+                ? this.makePipelinePreviewInstance()
+                : this.makePipelineEditorInstance();
+            this.prepareJsplumb(this.pipelineEditorInstance);
+            this.pipelineEditorBridge = new JsplumbBridge(
+                this.pipelineEditorInstance,
+            );
         }
+        return this.pipelineEditorBridge;
     }
 
     makePipelineEditorInstance(): BrowserJsPlumbInstance {
@@ -82,7 +69,7 @@ export class JsplumbFactoryService {
 
     makePipelinePreviewInstance(): BrowserJsPlumbInstance {
         return newInstance({
-            container: document.getElementById('assembly-preview'),
+            container: document.getElementById('assembly'),
             elementsDraggable: false,
         });
     }
@@ -93,13 +80,8 @@ export class JsplumbFactoryService {
         );
     }
 
-    destroy(preview: boolean) {
-        if (preview) {
-            this.pipelinePreviewInstance.destroy();
-            this.pipelinePreviewBridge = undefined;
-        } else {
-            this.pipelineEditorInstance.destroy();
-            this.pipelineEditorBridge = undefined;
-        }
+    destroy() {
+        this.pipelineEditorInstance?.destroy();
+        this.pipelineEditorBridge = undefined;
     }
 }
diff --git a/ui/src/app/editor/services/pipeline-positioning.service.ts 
b/ui/src/app/editor/services/pipeline-positioning.service.ts
index 1565101c72..52342e9e66 100644
--- a/ui/src/app/editor/services/pipeline-positioning.service.ts
+++ b/ui/src/app/editor/services/pipeline-positioning.service.ts
@@ -18,7 +18,6 @@
 
 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';
@@ -37,7 +36,6 @@ import { Connection } from '@jsplumb/browser-ui';
 export class PipelinePositioningService {
     constructor(
         private jsplumbService: JsplumbService,
-        private jsplumbConfigService: JsplumbConfigService,
         private jsplumbFactoryService: JsplumbFactoryService,
         private objectProvider: ObjectProvider,
     ) {}
@@ -87,8 +85,6 @@ export class PipelinePositioningService {
         const jsPlumbBridge =
             this.jsplumbFactoryService.getJsplumbBridge(previewConfig);
 
-        const jsplumbConfig = this.jsplumbConfigService.getEditorConfig();
-
         rawPipelineModel.forEach(currentPe => {
             if (!currentPe.settings.disabled) {
                 if (currentPe.type === 'stream' || currentPe.type === 'set') {
diff --git 
a/ui/src/app/pipeline-details/components/pipeline-details-expansion-panel/pipeline-element-details-row/elements/pipeline-elements-row.component.ts
 
b/ui/src/app/pipeline-details/components/pipeline-details-expansion-panel/pipeline-element-details-row/elements/pipeline-elements-row.component.ts
index 1bee34607b..a19c8e059c 100644
--- 
a/ui/src/app/pipeline-details/components/pipeline-details-expansion-panel/pipeline-element-details-row/elements/pipeline-elements-row.component.ts
+++ 
b/ui/src/app/pipeline-details/components/pipeline-details-expansion-panel/pipeline-element-details-row/elements/pipeline-elements-row.component.ts
@@ -24,7 +24,7 @@ import { PipelineElementTypeUtils } from 
'../../../../../editor/utils/editor.uti
     selector: 'sp-pipeline-elements-row',
     templateUrl: './pipeline-elements-row.component.html',
     styleUrls: [
-        
'../../../../../editor/components/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.scss',
+        
'../../../../../editor/components/pipeline-element-icon-stand/pipeline-element-icon-stand-row/pipeline-element-icon-stand-row.component.scss',
     ],
 })
 export class PipelineElementsRowComponent implements OnInit {
diff --git 
a/ui/src/app/pipeline-details/components/preview/pipeline-preview.component.html
 
b/ui/src/app/pipeline-details/components/preview/pipeline-preview.component.html
index ca4a4fc397..6ff4085506 100644
--- 
a/ui/src/app/pipeline-details/components/preview/pipeline-preview.component.html
+++ 
b/ui/src/app/pipeline-details/components/preview/pipeline-preview.component.html
@@ -16,16 +16,21 @@
   ~
   -->
 
-<div class="outer-assembly-preview">
-    <div class="pipeline-canvas-outer canvas-preview-inner">
-        <div id="{{ jspcanvas }}" class="canvas-preview">
-            <sp-pipeline
-                [canvasId]="jspcanvas"
-                [metricsInfo]="metricsInfo"
-                [rawPipelineModel]="rawPipelineModel"
-                [preview]="true"
-            ></sp-pipeline>
-        </div>
+<div class="outer-assembly-preview" fxFlex="100">
+    <div class="outerAssembly" fxFlex="100">
+        <sp-pipeline-assembly-drawing-area
+            *ngIf="rawPipelineModel"
+            fxFlex="100"
+            style="position: relative"
+            [jsplumbBridge]="jsPlumbBridge"
+            [metricsInfo]="metricsInfo"
+            [rawPipelineModel]="rawPipelineModel"
+            [pipelineCanvasMetadata]="pipelineCanvasMetadata"
+            [pipelineCanvasMetadataAvailable]="
+                pipelineCanvasMetadata !== undefined
+            "
+            [readonly]="true"
+        ></sp-pipeline-assembly-drawing-area>
     </div>
     <ng-content></ng-content>
 </div>
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 ce0208628d..3bea243bef 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
@@ -16,7 +16,14 @@
  *
  */
 
-import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import {
+    AfterViewInit,
+    Component,
+    EventEmitter,
+    Input,
+    OnInit,
+    Output,
+} from '@angular/core';
 import {
     Pipeline,
     PipelineCanvasMetadata,
@@ -26,20 +33,15 @@ import {
     PipelineElementConfig,
     PipelineElementUnion,
 } from '../../../editor/model/editor.model';
-import { PipelinePositioningService } from 
'../../../editor/services/pipeline-positioning.service';
 import { JsplumbService } from '../../../editor/services/jsplumb.service';
-import { ObjectProvider } from 
'../../../editor/services/object-provider.service';
-import { JsplumbFactoryService } from 
'../../../editor/services/jsplumb-factory.service';
+import { JsplumbBridge } from 
'../../../editor/services/jsplumb-bridge.service';
 
 @Component({
     selector: 'sp-pipeline-preview',
     templateUrl: './pipeline-preview.component.html',
     styleUrls: ['./pipeline-preview.component.scss'],
 })
-export class PipelinePreviewComponent implements OnInit {
-    @Input()
-    jspcanvas: string;
-
+export class PipelinePreviewComponent implements OnInit, AfterViewInit {
     @Input()
     metricsInfo: Record<string, SpMetricsEntry>;
 
@@ -55,52 +57,18 @@ export class PipelinePreviewComponent implements OnInit {
     selectedElementEmitter: EventEmitter<PipelineElementUnion> =
         new EventEmitter<PipelineElementUnion>();
 
-    constructor(
-        private pipelinePositioningService: PipelinePositioningService,
-        private jsplumbService: JsplumbService,
-        private jsplumbFactoryService: JsplumbFactoryService,
-        private objectProvider: ObjectProvider,
-    ) {}
+    jsPlumbBridge: JsplumbBridge;
+
+    constructor(private jsplumbService: JsplumbService) {}
+
+    ngAfterViewInit() {
+        this.jsPlumbBridge = this.jsplumbService.getBridge(true);
+    }
 
     ngOnInit() {
-        setTimeout(() => {
-            const canvasElementId = '#' + this.jspcanvas;
-            this.rawPipelineModel = this.jsplumbService.makeRawPipeline(
-                this.pipeline,
-                true,
-            );
-            setTimeout(() => {
-                this.pipelinePositioningService.displayPipeline(
-                    this.rawPipelineModel,
-                    canvasElementId,
-                    true,
-                    this.pipelineCanvasMetadata === undefined,
-                    this.pipelineCanvasMetadata,
-                );
-                const existingEndpointIds = [];
-                setTimeout(() => {
-                    this.jsplumbFactoryService
-                        .getJsplumbBridge(true)
-                        .selectEndpoints()
-                        .each(endpoint => {
-                            if (
-                                existingEndpointIds.indexOf(
-                                    endpoint.element.id,
-                                ) === -1
-                            ) {
-                                $(endpoint.element).click(() => {
-                                    const payload =
-                                        this.objectProvider.findElement(
-                                            endpoint.element.id,
-                                            this.rawPipelineModel,
-                                        ).payload;
-                                    this.selectedElementEmitter.emit(payload);
-                                });
-                                existingEndpointIds.push(endpoint.element.id);
-                            }
-                        });
-                });
-            });
-        });
+        this.rawPipelineModel = this.jsplumbService.makeRawPipeline(
+            this.pipeline,
+            true,
+        );
     }
 }
diff --git a/ui/src/app/pipeline-details/pipeline-details.component.html 
b/ui/src/app/pipeline-details/pipeline-details.component.html
index f07f3464a6..9e38234ea6 100644
--- a/ui/src/app/pipeline-details/pipeline-details.component.html
+++ b/ui/src/app/pipeline-details/pipeline-details.component.html
@@ -32,7 +32,6 @@
     </div>
     <div fxFlex="100" fxLayout="column">
         <sp-pipeline-preview
-            [jspcanvas]="'assembly-preview'"
             [metricsInfo]="metricsInfo"
             [pipeline]="pipeline"
             [pipelineCanvasMetadata]="pipelineCanvasMetadata"

Reply via email to