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

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


The following commit(s) were added to refs/heads/dev by this push:
     new 3472bcb25b feat(#3105): Allow modification of running pipelines (#3106)
3472bcb25b is described below

commit 3472bcb25bd9377799470d17b11a0a689489e430
Author: Dominik Riemer <[email protected]>
AuthorDate: Thu Aug 8 12:39:49 2024 +0200

    feat(#3105): Allow modification of running pipelines (#3106)
    
    * feat(#3105): Allow modification of running pipelines
    
    * Remove unused export
    
    * Add e2e test
---
 .../rest/impl/PipelineCanvasMetadataResource.java  |   2 +
 ui/cypress/support/utils/PipelineUtils.ts          |  31 +-
 .../pipeline/updatePipelineTest.smoke.spec.ts      |  67 +++++
 .../authentication-configuration.component.ts      |   1 -
 .../edit-schema-transformation.component.ts        |   1 -
 ui/src/app/core-ui/core-ui.module.ts               |  12 +
 .../loading-indicator.component.html               |  22 ++
 .../loading-indicator.component.scss}              |  28 +-
 .../loading-indicator.component.ts}                |  35 +--
 .../multi-step-status-indicator.component.html     |  47 ++++
 .../multi-step-status-indicator.component.scss}    |  30 +-
 .../multi-step-status-indicator.component.ts}      |  37 +--
 .../multi-step-status-indicator.model.ts}          |  32 +--
 .../pipeline-operation-status.component.html       |  56 ++++
 .../pipeline-operation-status.component.scss}      |  29 +-
 .../pipeline-operation-status.component.ts}        |  36 +--
 .../pipeline-started-status.component.html         |  57 ++--
 .../pipeline-started-status.component.ts           |   3 +
 .../status-indicator.component.html                |  23 ++
 .../status-indicator.component.scss}               |  30 +-
 .../status-indicator.component.ts}                 |  37 +--
 .../pipeline-assembly.component.ts                 | 177 ++++++------
 .../save-pipeline-settings.component.html          |  86 ++++++
 .../save-pipeline-settings.component.scss}         |  29 --
 .../save-pipeline-settings.component.ts            |  82 ++++++
 .../save-pipeline/save-pipeline.component.html     | 164 ++++-------
 .../save-pipeline/save-pipeline.component.scss     |   9 -
 .../save-pipeline/save-pipeline.component.ts       | 311 ++++++++++++++-------
 ui/src/app/editor/editor.component.html            |  30 +-
 ui/src/app/editor/editor.component.ts              |  19 +-
 ui/src/app/editor/editor.module.ts                 |   2 +
 ui/src/app/editor/model/editor.model.ts            |  23 +-
 .../pipeline-overview.component.html               |   5 +-
 .../pipeline-overview.component.ts                 |  13 -
 .../pipeline-status-dialog.component.ts            |   5 -
 ui/src/app/pipelines/pipelines.component.html      |   1 -
 ui/src/app/pipelines/pipelines.component.ts        |  36 +--
 .../tour/create-pipeline-tour.constants.ts         |   2 +-
 ui/src/app/services/tour/shepherd.service.ts       |   8 -
 ui/src/scss/sp/main.scss                           |   4 +-
 ui/src/scss/sp/shepherd-new.scss                   |   5 +-
 41 files changed, 910 insertions(+), 717 deletions(-)

diff --git 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCanvasMetadataResource.java
 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCanvasMetadataResource.java
index 20a6e35a0a..5ba0edcbbb 100644
--- 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCanvasMetadataResource.java
+++ 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCanvasMetadataResource.java
@@ -95,6 +95,8 @@ public class PipelineCanvasMetadataResource extends 
AbstractRestResource {
   public ResponseEntity<Void> 
updatePipelineCanvasMetadata(@PathVariable("canvasId") String pipelineCanvasId,
                                                            @RequestBody 
PipelineCanvasMetadata pipelineCanvasMetadata) {
     try {
+      var existing = 
getPipelineCanvasMetadataStorage().getElementById(pipelineCanvasMetadata.getId());
+      pipelineCanvasMetadata.setRev(existing.getRev());
       getPipelineCanvasMetadataStorage().updateElement(pipelineCanvasMetadata);
     } catch (IllegalArgumentException e) {
       getPipelineCanvasMetadataStorage().persist(pipelineCanvasMetadata);
diff --git a/ui/cypress/support/utils/PipelineUtils.ts 
b/ui/cypress/support/utils/PipelineUtils.ts
index cb132e9347..846dd478ca 100644
--- a/ui/cypress/support/utils/PipelineUtils.ts
+++ b/ui/cypress/support/utils/PipelineUtils.ts
@@ -32,6 +32,10 @@ export class PipelineUtils {
         PipelineUtils.startPipeline(pipelineInput);
     }
 
+    public static editPipeline() {
+        cy.dataCy('modify-pipeline-btn').first().click();
+    }
+
     public static goToPipelines() {
         cy.visit('#/pipelines');
     }
@@ -99,16 +103,31 @@ export class PipelineUtils {
         cy.dataCy('sp-element-configuration-save').click();
     }
 
-    private static startPipeline(pipelineInput: PipelineInput) {
+    public static startPipeline(pipelineInput?: PipelineInput) {
         // Save and start pipeline
         cy.dataCy('sp-editor-save-pipeline').click();
-        cy.dataCy('sp-editor-pipeline-name').type(pipelineInput.pipelineName);
-        cy.dataCy('sp-editor-checkbox-start-immediately').children().click();
-        cy.dataCy('sp-editor-save').click();
-        cy.dataCy('sp-pipeline-started-dialog', { timeout: 15000 }).should(
+        if (pipelineInput) {
+            cy.dataCy('sp-editor-pipeline-name').type(
+                pipelineInput.pipelineName,
+            );
+        }
+        PipelineUtils.finalizePipelineStart();
+    }
+
+    public static clonePipeline(newPipelineName: string) {
+        cy.dataCy('pipeline-update-mode-clone').children().click();
+        cy.dataCy('sp-editor-pipeline-name').type(newPipelineName);
+    }
+
+    public static finalizePipelineStart() {
+        
cy.dataCy('sp-editor-checkbox-navigate-to-overview').children().click();
+        cy.dataCy('sp-editor-apply').click();
+        cy.dataCy('sp-pipeline-started-success', { timeout: 15000 }).should(
             'be.visible',
         );
-        cy.dataCy('sp-pipeline-dialog-close', { timeout: 15000 }).click();
+        cy.dataCy('sp-navigate-to-pipeline-overview', {
+            timeout: 15000,
+        }).click();
     }
 
     public static checkAmountOfPipelinesPipeline(amount: number) {
diff --git a/ui/cypress/tests/pipeline/updatePipelineTest.smoke.spec.ts 
b/ui/cypress/tests/pipeline/updatePipelineTest.smoke.spec.ts
new file mode 100644
index 0000000000..b2e69bd0b4
--- /dev/null
+++ b/ui/cypress/tests/pipeline/updatePipelineTest.smoke.spec.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 { ConnectUtils } from '../../support/utils/connect/ConnectUtils';
+import { PipelineUtils } from '../../support/utils/PipelineUtils';
+import { PipelineElementBuilder } from 
'../../support/builder/PipelineElementBuilder';
+import { PipelineBuilder } from '../../support/builder/PipelineBuilder';
+
+const adapterName = 'simulator';
+
+describe('Test update of running pipeline', () => {
+    beforeEach('Setup Test', () => {
+        cy.initStreamPipesTest();
+        ConnectUtils.addMachineDataSimulator(adapterName);
+    });
+
+    it('Perform Test', () => {
+        const pipelineInput = PipelineBuilder.create('Pipeline Test')
+            .addSource(adapterName)
+            .addProcessingElement(
+                PipelineElementBuilder.create('field_renamer')
+                    .addInput('drop-down', 'convert-property', 'timestamp')
+                    .addInput('input', 'field-name', 't')
+                    .build(),
+            )
+            .addSink(
+                PipelineElementBuilder.create('data_lake')
+                    .addInput('input', 'db_measurement', 'demo')
+                    .build(),
+            )
+            .build();
+
+        PipelineUtils.addPipeline(pipelineInput);
+        PipelineUtils.editPipeline();
+        cy.wait(1000);
+        PipelineUtils.startPipeline();
+        cy.dataCy('modify-pipeline-btn', { timeout: 10000 }).should(
+            'have.length',
+            1,
+        );
+
+        PipelineUtils.editPipeline();
+        cy.wait(1000);
+        cy.dataCy('sp-editor-save-pipeline').click();
+        PipelineUtils.clonePipeline('Pipeline Test 2');
+        PipelineUtils.finalizePipelineStart();
+        cy.dataCy('modify-pipeline-btn', { timeout: 10000 }).should(
+            'have.length',
+            2,
+        );
+    });
+});
diff --git 
a/ui/src/app/configuration/security-configuration/authentication-configuration/authentication-configuration.component.ts
 
b/ui/src/app/configuration/security-configuration/authentication-configuration/authentication-configuration.component.ts
index b07187eebe..9ad15881ee 100644
--- 
a/ui/src/app/configuration/security-configuration/authentication-configuration/authentication-configuration.component.ts
+++ 
b/ui/src/app/configuration/security-configuration/authentication-configuration/authentication-configuration.component.ts
@@ -30,7 +30,6 @@ export class SecurityAuthenticationConfigurationComponent {
 
     generateKeyPair() {
         this.configurationService.generateKeyPair().subscribe(result => {
-            console.log(result);
             this.saveKeyfile('public.key', result[0]);
             this.saveKeyfile('private.pem', result[1]);
         });
diff --git 
a/ui/src/app/connect/dialog/edit-event-property/components/edit-schema-transformation/edit-schema-transformation.component.ts
 
b/ui/src/app/connect/dialog/edit-event-property/components/edit-schema-transformation/edit-schema-transformation.component.ts
index 608b6c4124..7c262a8e2f 100644
--- 
a/ui/src/app/connect/dialog/edit-event-property/components/edit-schema-transformation/edit-schema-transformation.component.ts
+++ 
b/ui/src/app/connect/dialog/edit-event-property/components/edit-schema-transformation/edit-schema-transformation.component.ts
@@ -91,7 +91,6 @@ export class EditSchemaTransformationComponent implements 
OnInit {
     }
 
     triggerTutorialStep(): void {
-        console.log('lu');
         if (this.cachedProperty.runtimeName === 'temp') {
             this.shepherdService.trigger('adapter-runtime-name-changed');
         }
diff --git a/ui/src/app/core-ui/core-ui.module.ts 
b/ui/src/app/core-ui/core-ui.module.ts
index 05aa78f741..85c08d4157 100644
--- a/ui/src/app/core-ui/core-ui.module.ts
+++ b/ui/src/app/core-ui/core-ui.module.ts
@@ -105,6 +105,10 @@ import { MatTooltipModule } from 
'@angular/material/tooltip';
 import { MatProgressBarModule } from '@angular/material/progress-bar';
 import { MatButtonToggleModule } from '@angular/material/button-toggle';
 import { StaticRuntimeResolvableGroupComponent } from 
'./static-properties/static-runtime-resolvable-group/static-runtime-resolvable-group.component';
+import { LoadingIndicatorComponent } from 
'./loading-indicator/loading-indicator.component';
+import { StatusIndicatorComponent } from 
'./status-indicator/status-indicator.component';
+import { MultiStepStatusIndicatorComponent } from 
'./multi-step-status-indicator/multi-step-status-indicator.component';
+import { PipelineOperationStatusComponent } from 
'./pipeline/pipeline-operation-status/pipeline-operation-status.component';
 
 @NgModule({
     imports: [
@@ -198,6 +202,10 @@ import { StaticRuntimeResolvableGroupComponent } from 
'./static-properties/stati
         LivePreviewLoadingComponent,
         LivePreviewTableComponent,
         LivePreviewErrorComponent,
+        LoadingIndicatorComponent,
+        StatusIndicatorComponent,
+        MultiStepStatusIndicatorComponent,
+        PipelineOperationStatusComponent,
     ],
     providers: [MatDatepickerModule, DisplayRecommendedPipe],
     exports: [
@@ -228,6 +236,10 @@ import { StaticRuntimeResolvableGroupComponent } from 
'./static-properties/stati
         SpSimpleLogsComponent,
         SpSimpleMetricsComponent,
         StatusWidgetComponent,
+        LoadingIndicatorComponent,
+        StatusIndicatorComponent,
+        MultiStepStatusIndicatorComponent,
+        PipelineOperationStatusComponent,
     ],
 })
 export class CoreUiModule {}
diff --git 
a/ui/src/app/core-ui/loading-indicator/loading-indicator.component.html 
b/ui/src/app/core-ui/loading-indicator/loading-indicator.component.html
new file mode 100644
index 0000000000..3982ccb2ac
--- /dev/null
+++ b/ui/src/app/core-ui/loading-indicator/loading-indicator.component.html
@@ -0,0 +1,22 @@
+<!--
+  ~ 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 fxLayout="column" fxFlex="100" fxLayoutAlign="center center">
+    <mat-spinner [diameter]="30" color="accent"></mat-spinner>
+    <div class="message-text">{{ message }}</div>
+</div>
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss 
b/ui/src/app/core-ui/loading-indicator/loading-indicator.component.scss
similarity index 72%
copy from ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
copy to ui/src/app/core-ui/loading-indicator/loading-indicator.component.scss
index 5b19b82153..ab46954fee 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
+++ b/ui/src/app/core-ui/loading-indicator/loading-indicator.component.scss
@@ -16,31 +16,7 @@
  *
  */
 
-@import '../../../../scss/sp/sp-dialog.scss';
-
-.customize-section {
-    display: flex;
-    flex: 1 1 auto;
-    padding: 20px;
-}
-
-.padding-20 {
-    padding: 20px;
-}
-
-.mb-10 {
-    margin-bottom: 10px;
-}
-
-::ng-deep .pipeline-radio-group .mat-radio-label {
-    padding: 0;
-}
-
-.status-text {
-    font-size: 14pt;
-    margin-top: 10px;
-}
-
-.status-subtext {
+.message-text {
     font-size: 12pt;
+    margin-top: 10px;
 }
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss 
b/ui/src/app/core-ui/loading-indicator/loading-indicator.component.ts
similarity index 68%
copy from ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
copy to ui/src/app/core-ui/loading-indicator/loading-indicator.component.ts
index 5b19b82153..1378cf7811 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
+++ b/ui/src/app/core-ui/loading-indicator/loading-indicator.component.ts
@@ -16,31 +16,14 @@
  *
  */
 
-@import '../../../../scss/sp/sp-dialog.scss';
+import { Component, Input } from '@angular/core';
 
-.customize-section {
-    display: flex;
-    flex: 1 1 auto;
-    padding: 20px;
-}
-
-.padding-20 {
-    padding: 20px;
-}
-
-.mb-10 {
-    margin-bottom: 10px;
-}
-
-::ng-deep .pipeline-radio-group .mat-radio-label {
-    padding: 0;
-}
-
-.status-text {
-    font-size: 14pt;
-    margin-top: 10px;
-}
-
-.status-subtext {
-    font-size: 12pt;
+@Component({
+    selector: 'sp-loading-indicator',
+    templateUrl: './loading-indicator.component.html',
+    styleUrls: ['./loading-indicator.component.scss'],
+})
+export class LoadingIndicatorComponent {
+    @Input()
+    message = 'Loading';
 }
diff --git 
a/ui/src/app/core-ui/multi-step-status-indicator/multi-step-status-indicator.component.html
 
b/ui/src/app/core-ui/multi-step-status-indicator/multi-step-status-indicator.component.html
new file mode 100644
index 0000000000..139340253c
--- /dev/null
+++ 
b/ui/src/app/core-ui/multi-step-status-indicator/multi-step-status-indicator.component.html
@@ -0,0 +1,47 @@
+<!--
+  ~ 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 fxLayout="column" fxLayoutGap="10px">
+    <div fxLayout="column" *ngFor="let step of statusIndicators">
+        <div
+            fxLayout="row"
+            fxLayoutGap="30px"
+            fxFlex="100"
+            fxLayoutAlign="start center"
+        >
+            <div *ngIf="step.status === Status.PROGRESS">
+                <mat-spinner [diameter]="25" color="accent"></mat-spinner>
+            </div>
+            <div
+                *ngIf="step.status === Status.SUCCESS"
+                fxLayoutAlign="start center"
+            >
+                <i class="material-icons status-icon">check_circle</i>
+            </div>
+            <div
+                *ngIf="step.status === Status.FAILURE"
+                fxLayoutAlign="start centers"
+            >
+                <i class="material-icons status-icon">error</i>
+            </div>
+            <div fxFlex class="message-text" fxLayoutAlign="start center">
+                {{ step.message }}
+            </div>
+        </div>
+    </div>
+</div>
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss 
b/ui/src/app/core-ui/multi-step-status-indicator/multi-step-status-indicator.component.scss
similarity index 70%
copy from ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
copy to 
ui/src/app/core-ui/multi-step-status-indicator/multi-step-status-indicator.component.scss
index 5b19b82153..b3412ccbee 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
+++ 
b/ui/src/app/core-ui/multi-step-status-indicator/multi-step-status-indicator.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,31 +16,11 @@
  *
  */
 
-@import '../../../../scss/sp/sp-dialog.scss';
-
-.customize-section {
-    display: flex;
-    flex: 1 1 auto;
-    padding: 20px;
-}
-
-.padding-20 {
-    padding: 20px;
-}
-
-.mb-10 {
-    margin-bottom: 10px;
-}
-
-::ng-deep .pipeline-radio-group .mat-radio-label {
-    padding: 0;
-}
-
-.status-text {
-    font-size: 14pt;
-    margin-top: 10px;
+.status-icon {
+    font-size: 22pt;
+    color: var(--color-accent);
 }
 
-.status-subtext {
+.message-text {
     font-size: 12pt;
 }
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss 
b/ui/src/app/core-ui/multi-step-status-indicator/multi-step-status-indicator.component.ts
similarity index 63%
copy from ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
copy to 
ui/src/app/core-ui/multi-step-status-indicator/multi-step-status-indicator.component.ts
index 5b19b82153..c297973fb4 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
+++ 
b/ui/src/app/core-ui/multi-step-status-indicator/multi-step-status-indicator.component.ts
@@ -16,31 +16,16 @@
  *
  */
 
-@import '../../../../scss/sp/sp-dialog.scss';
+import { Component, Input } from '@angular/core';
+import { Status, StatusIndicator } from './multi-step-status-indicator.model';
 
-.customize-section {
-    display: flex;
-    flex: 1 1 auto;
-    padding: 20px;
-}
-
-.padding-20 {
-    padding: 20px;
-}
-
-.mb-10 {
-    margin-bottom: 10px;
-}
-
-::ng-deep .pipeline-radio-group .mat-radio-label {
-    padding: 0;
-}
-
-.status-text {
-    font-size: 14pt;
-    margin-top: 10px;
-}
-
-.status-subtext {
-    font-size: 12pt;
+@Component({
+    selector: 'sp-multi-step-status-indicator',
+    templateUrl: './multi-step-status-indicator.component.html',
+    styleUrls: ['./multi-step-status-indicator.component.scss'],
+})
+export class MultiStepStatusIndicatorComponent {
+    @Input()
+    statusIndicators: StatusIndicator[] = [];
+    protected readonly Status = Status;
 }
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss 
b/ui/src/app/core-ui/multi-step-status-indicator/multi-step-status-indicator.model.ts
similarity index 68%
copy from ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
copy to 
ui/src/app/core-ui/multi-step-status-indicator/multi-step-status-indicator.model.ts
index 5b19b82153..9e9c7f1d52 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
+++ 
b/ui/src/app/core-ui/multi-step-status-indicator/multi-step-status-indicator.model.ts
@@ -16,31 +16,13 @@
  *
  */
 
-@import '../../../../scss/sp/sp-dialog.scss';
-
-.customize-section {
-    display: flex;
-    flex: 1 1 auto;
-    padding: 20px;
-}
-
-.padding-20 {
-    padding: 20px;
-}
-
-.mb-10 {
-    margin-bottom: 10px;
-}
-
-::ng-deep .pipeline-radio-group .mat-radio-label {
-    padding: 0;
-}
-
-.status-text {
-    font-size: 14pt;
-    margin-top: 10px;
+export enum Status {
+    PROGRESS,
+    SUCCESS,
+    FAILURE,
 }
 
-.status-subtext {
-    font-size: 12pt;
+export interface StatusIndicator {
+    message: string;
+    status: Status;
 }
diff --git 
a/ui/src/app/core-ui/pipeline/pipeline-operation-status/pipeline-operation-status.component.html
 
b/ui/src/app/core-ui/pipeline/pipeline-operation-status/pipeline-operation-status.component.html
new file mode 100644
index 0000000000..914bc3f8be
--- /dev/null
+++ 
b/ui/src/app/core-ui/pipeline/pipeline-operation-status/pipeline-operation-status.component.html
@@ -0,0 +1,56 @@
+<!--
+  ~ 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="column"
+    class="status-outer mt-10"
+    *ngFor="let msg of pipelineOperationStatus.elementStatus"
+>
+    <div fxFlex="100" fxLayout="column">
+        <div
+            fxFlex="100"
+            fxLayout="row"
+            fxLayoutAlign="start center"
+            class="p-15"
+        >
+            <mat-icon color="accent" *ngIf="msg.success">done</mat-icon>
+            <mat-icon style="color: red" 
*ngIf="!msg.success">warning</mat-icon>
+            <div fxFlex="100" fxLayout="column" class="ml-5">
+                <span
+                    ><b>{{ msg.elementName }}</b></span
+                >
+                <small>{{
+                    msg.elementId.substr(0, msg.elementId.lastIndexOf('/'))
+                }}</small>
+            </div>
+        </div>
+        <div>
+            <div
+                fxFlex="100"
+                fxLayout="column"
+                *ngIf="msg.optionalMessage"
+                class="mt-10"
+            >
+                <div class="error-message">
+                    <div class="p-10">{{ msg.optionalMessage }}</div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss 
b/ui/src/app/core-ui/pipeline/pipeline-operation-status/pipeline-operation-status.component.scss
similarity index 68%
copy from ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
copy to 
ui/src/app/core-ui/pipeline/pipeline-operation-status/pipeline-operation-status.component.scss
index 5b19b82153..16a5951fa8 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
+++ 
b/ui/src/app/core-ui/pipeline/pipeline-operation-status/pipeline-operation-status.component.scss
@@ -16,31 +16,6 @@
  *
  */
 
-@import '../../../../scss/sp/sp-dialog.scss';
-
-.customize-section {
-    display: flex;
-    flex: 1 1 auto;
-    padding: 20px;
-}
-
-.padding-20 {
-    padding: 20px;
-}
-
-.mb-10 {
-    margin-bottom: 10px;
-}
-
-::ng-deep .pipeline-radio-group .mat-radio-label {
-    padding: 0;
-}
-
-.status-text {
-    font-size: 14pt;
-    margin-top: 10px;
-}
-
-.status-subtext {
-    font-size: 12pt;
+.status-outer {
+    border: 1px solid var(--color-bg-3);
 }
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss 
b/ui/src/app/core-ui/pipeline/pipeline-operation-status/pipeline-operation-status.component.ts
similarity index 65%
copy from ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
copy to 
ui/src/app/core-ui/pipeline/pipeline-operation-status/pipeline-operation-status.component.ts
index 5b19b82153..72324f8652 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
+++ 
b/ui/src/app/core-ui/pipeline/pipeline-operation-status/pipeline-operation-status.component.ts
@@ -16,31 +16,15 @@
  *
  */
 
-@import '../../../../scss/sp/sp-dialog.scss';
+import { Component, Input } from '@angular/core';
+import { PipelineOperationStatus } from '@streampipes/platform-services';
 
-.customize-section {
-    display: flex;
-    flex: 1 1 auto;
-    padding: 20px;
-}
-
-.padding-20 {
-    padding: 20px;
-}
-
-.mb-10 {
-    margin-bottom: 10px;
-}
-
-::ng-deep .pipeline-radio-group .mat-radio-label {
-    padding: 0;
-}
-
-.status-text {
-    font-size: 14pt;
-    margin-top: 10px;
-}
-
-.status-subtext {
-    font-size: 12pt;
+@Component({
+    selector: 'sp-pipeline-operation-status',
+    templateUrl: './pipeline-operation-status.component.html',
+    styleUrls: ['./pipeline-operation-status.component.scss'],
+})
+export class PipelineOperationStatusComponent {
+    @Input()
+    pipelineOperationStatus: PipelineOperationStatus;
 }
diff --git 
a/ui/src/app/core-ui/pipeline/pipeline-started-status/pipeline-started-status.component.html
 
b/ui/src/app/core-ui/pipeline/pipeline-started-status/pipeline-started-status.component.html
index 9ff6b42b6d..85f64484d8 100644
--- 
a/ui/src/app/core-ui/pipeline/pipeline-started-status/pipeline-started-status.component.html
+++ 
b/ui/src/app/core-ui/pipeline/pipeline-started-status/pipeline-started-status.component.html
@@ -25,7 +25,10 @@
         data-cy="sp-pipeline-started-dialog"
     >
         <div fxLayout="row">
-            <mat-icon color="accent" *ngIf="pipelineOperationStatus.success"
+            <mat-icon
+                data-cy="sp-pipeline-started-success"
+                color="accent"
+                *ngIf="pipelineOperationStatus.success"
                 >done</mat-icon
             >
             <mat-icon
@@ -36,7 +39,11 @@
             <span>&nbsp;{{ pipelineOperationStatus.title }}.</span>
         </div>
         <span
-            *ngIf="action === 1 && !pipelineOperationStatus.success"
+            *ngIf="
+                action === 1 &&
+                !pipelineOperationStatus.success &&
+                !forceStopDisabled
+            "
             class="message-small"
             >You can perform a forced stop, which will stop and reset the
             pipeline status.</span
@@ -58,7 +65,11 @@
             class="ml-10"
             color="accent"
             (click)="forceStopPipeline()"
-            *ngIf="action === 1 && !pipelineOperationStatus.success"
+            *ngIf="
+                action === 1 &&
+                !pipelineOperationStatus.success &&
+                !forceStopDisabled
+            "
         >
             <div>Force stop</div>
         </button>
@@ -70,43 +81,9 @@
         *ngIf="statusDetailsVisible"
         class="w-100"
     >
-        <div
-            fxFlex="100"
-            fxLayout="column"
-            class="mat-elevation-z1 mt-10"
-            *ngFor="let msg of pipelineOperationStatus.elementStatus"
+        <sp-pipeline-operation-status
+            [pipelineOperationStatus]="pipelineOperationStatus"
         >
-            <div fxFlex="100" fxLayout="column" class="p-15">
-                <div fxFlex="100" fxLayout="row" fxLayoutAlign="start center">
-                    <mat-icon color="accent" 
*ngIf="msg.success">done</mat-icon>
-                    <mat-icon style="color: red" *ngIf="!msg.success"
-                        >warning</mat-icon
-                    >
-                    <div fxFlex="100" fxLayout="column" class="ml-5">
-                        <span
-                            ><b>{{ msg.elementName }}</b></span
-                        >
-                        <small>{{
-                            msg.elementId.substr(
-                                0,
-                                msg.elementId.lastIndexOf('/')
-                            )
-                        }}</small>
-                    </div>
-                </div>
-                <div>
-                    <div
-                        fxFlex="100"
-                        fxLayout="column"
-                        *ngIf="msg.optionalMessage"
-                        class="mt-10"
-                    >
-                        <div class="error-message">
-                            {{ msg.optionalMessage }}
-                        </div>
-                    </div>
-                </div>
-            </div>
-        </div>
+        </sp-pipeline-operation-status>
     </div>
 </div>
diff --git 
a/ui/src/app/core-ui/pipeline/pipeline-started-status/pipeline-started-status.component.ts
 
b/ui/src/app/core-ui/pipeline/pipeline-started-status/pipeline-started-status.component.ts
index d36360323c..c281426a6b 100644
--- 
a/ui/src/app/core-ui/pipeline/pipeline-started-status/pipeline-started-status.component.ts
+++ 
b/ui/src/app/core-ui/pipeline/pipeline-started-status/pipeline-started-status.component.ts
@@ -32,6 +32,9 @@ export class PipelineStartedStatusComponent implements OnInit 
{
     @Input()
     action: PipelineAction;
 
+    @Input()
+    forceStopDisabled = false;
+
     @Output()
     forceStopPipelineEmitter = new EventEmitter();
 
diff --git 
a/ui/src/app/core-ui/status-indicator/status-indicator.component.html 
b/ui/src/app/core-ui/status-indicator/status-indicator.component.html
new file mode 100644
index 0000000000..bbb27508b0
--- /dev/null
+++ b/ui/src/app/core-ui/status-indicator/status-indicator.component.html
@@ -0,0 +1,23 @@
+<!--
+  ~ 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 fxLayout="column" fxFlex="100" fxLayoutAlign="center center">
+    <i class="material-icons status-icon">{{ icon }}</i>
+    <div class="status-text">{{ message }}</div>
+    <div class="status-subtext">{{ additionalDescription }}</div>
+</div>
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss 
b/ui/src/app/core-ui/status-indicator/status-indicator.component.scss
similarity index 75%
copy from ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
copy to ui/src/app/core-ui/status-indicator/status-indicator.component.scss
index 5b19b82153..30ab52e3a0 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
+++ b/ui/src/app/core-ui/status-indicator/status-indicator.component.scss
@@ -16,31 +16,17 @@
  *
  */
 
-@import '../../../../scss/sp/sp-dialog.scss';
-
-.customize-section {
-    display: flex;
-    flex: 1 1 auto;
-    padding: 20px;
-}
-
-.padding-20 {
-    padding: 20px;
-}
-
-.mb-10 {
-    margin-bottom: 10px;
-}
-
-::ng-deep .pipeline-radio-group .mat-radio-label {
-    padding: 0;
-}
-
 .status-text {
-    font-size: 14pt;
+    font-size: 12pt;
     margin-top: 10px;
 }
 
 .status-subtext {
-    font-size: 12pt;
+    margin-top: 10px;
+    font-size: 10pt;
+}
+
+.status-icon {
+    font-size: 22pt;
+    color: var(--color-accent);
 }
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss 
b/ui/src/app/core-ui/status-indicator/status-indicator.component.ts
similarity index 68%
copy from ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
copy to ui/src/app/core-ui/status-indicator/status-indicator.component.ts
index 5b19b82153..fd89fccdc8 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
+++ b/ui/src/app/core-ui/status-indicator/status-indicator.component.ts
@@ -16,31 +16,20 @@
  *
  */
 
-@import '../../../../scss/sp/sp-dialog.scss';
+import { Component, Input } from '@angular/core';
 
-.customize-section {
-    display: flex;
-    flex: 1 1 auto;
-    padding: 20px;
-}
-
-.padding-20 {
-    padding: 20px;
-}
-
-.mb-10 {
-    margin-bottom: 10px;
-}
-
-::ng-deep .pipeline-radio-group .mat-radio-label {
-    padding: 0;
-}
+@Component({
+    selector: 'sp-status-indicator',
+    templateUrl: './status-indicator.component.html',
+    styleUrls: ['./status-indicator.component.scss'],
+})
+export class StatusIndicatorComponent {
+    @Input()
+    message = 'Loading';
 
-.status-text {
-    font-size: 14pt;
-    margin-top: 10px;
-}
+    @Input()
+    additionalDescription = '';
 
-.status-subtext {
-    font-size: 12pt;
+    @Input()
+    icon: string;
 }
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 4f138e2694..fa5c9afa1e 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
@@ -23,6 +23,7 @@ import {
     EventEmitter,
     Input,
     NgZone,
+    OnDestroy,
     OnInit,
     Output,
     ViewChild,
@@ -31,7 +32,6 @@ 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 { ShepherdService } from '../../../services/tour/shepherd.service';
 import {
     PipelineElementConfig,
     PipelineElementUnion,
@@ -47,6 +47,7 @@ import { SavePipelineComponent } from 
'../../dialog/save-pipeline/save-pipeline.
 import { MatDialog } from '@angular/material/dialog';
 import { EditorService } from '../../services/editor.service';
 import {
+    Pipeline,
     PipelineCanvasMetadata,
     PipelineCanvasMetadataService,
     PipelineService,
@@ -55,22 +56,25 @@ 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 } from 'rxjs';
+import { forkJoin, of, Subscription } from 'rxjs';
 import { PipelineElementDiscoveryComponent } from 
'../../dialog/pipeline-element-discovery/pipeline-element-discovery.component';
 import { SpPipelineRoutes } from '../../../pipelines/pipelines.routes';
 import { Router } from '@angular/router';
+import { catchError } from 'rxjs/operators';
 
 @Component({
     selector: 'sp-pipeline-assembly',
     templateUrl: './pipeline-assembly.component.html',
     styleUrls: ['./pipeline-assembly.component.scss'],
 })
-export class PipelineAssemblyComponent implements OnInit, AfterViewInit {
+export class PipelineAssemblyComponent
+    implements OnInit, AfterViewInit, OnDestroy
+{
     @Input()
     rawPipelineModel: PipelineElementConfig[];
 
     @Input()
-    currentModifiedPipelineId: any;
+    currentModifiedPipelineId: string;
 
     @Input()
     allElements: PipelineElementUnion[];
@@ -80,17 +84,11 @@ export class PipelineAssemblyComponent implements OnInit, 
AfterViewInit {
         new EventEmitter<boolean>();
 
     JsplumbBridge: JsplumbBridge;
-
-    pipelineCanvasMaximized = false;
-
-    currentMouseOverElement: any;
     currentZoomLevel: any;
     preview: any;
 
     selectMode: any;
-    currentPipelineName: any;
-    currentPipelineDescription: any;
-
+    originalPipeline: Pipeline;
     pipelineValid = false;
 
     pipelineCacheRunning = false;
@@ -107,6 +105,7 @@ export class PipelineAssemblyComponent implements OnInit, 
AfterViewInit {
     pipelineComponent: PipelineComponent;
 
     panzoom: PanzoomObject;
+    moveSub: Subscription;
 
     constructor(
         private jsPlumbFactoryService: JsplumbFactoryService,
@@ -116,14 +115,13 @@ export class PipelineAssemblyComponent implements OnInit, 
AfterViewInit {
         public pipelineValidationService: PipelineValidationService,
         private pipelineService: PipelineService,
         private jsplumbService: JsplumbService,
-        private shepherdService: ShepherdService,
         private dialogService: DialogService,
         private dialog: MatDialog,
         private ngZone: NgZone,
+        private router: Router,
         private pipelineElementDraggedService: PipelineElementDraggedService,
         private pipelineCanvasMetadataService: PipelineCanvasMetadataService,
         private breadcrumbService: SpBreadcrumbService,
-        private router: Router,
     ) {
         this.selectMode = true;
         this.currentZoomLevel = 1;
@@ -131,30 +129,31 @@ export class PipelineAssemblyComponent implements OnInit, 
AfterViewInit {
 
     ngOnInit(): void {
         if (this.currentModifiedPipelineId) {
-            this.displayPipelineById();
+            this.displayPipelineById(this.currentModifiedPipelineId);
         } else {
             this.checkAndDisplayCachedPipeline();
         }
-        
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);
-                }
-            },
-        );
+        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() {
@@ -221,20 +220,15 @@ export class PipelineAssemblyComponent implements OnInit, 
AfterViewInit {
      * clears the Assembly of all elements
      */
     clearAssembly() {
-        // $('#assembly').children().not('#clear, #submit').remove();
         this.JsplumbBridge.deleteEveryEndpoint();
         this.rawPipelineModel = [];
         this.currentZoomLevel = 1;
         this.JsplumbBridge.setZoom(this.currentZoomLevel);
         this.JsplumbBridge.repaintEverything();
 
-        const removePipelineFromCache =
-            this.editorService.removePipelineFromCache();
-        const removeCanvasMetadataFromCache =
-            this.editorService.removeCanvasMetadataFromCache();
         forkJoin([
-            removePipelineFromCache,
-            removeCanvasMetadataFromCache,
+            this.editorService.removePipelineFromCache(),
+            this.editorService.removeCanvasMetadataFromCache(),
         ]).subscribe(msg => {
             this.pipelineCached = false;
             this.pipelineCacheRunning = false;
@@ -255,21 +249,30 @@ export class PipelineAssemblyComponent implements OnInit, 
AfterViewInit {
             pipelineModel,
             this.preview,
         );
-        pipeline.name = this.currentPipelineName;
-        pipeline.description = this.currentPipelineDescription;
-        if (this.currentModifiedPipelineId) {
-            pipeline._id = this.currentModifiedPipelineId;
-        }
-
-        this.dialogService.open(SavePipelineComponent, {
+        const dialogRef = this.dialogService.open(SavePipelineComponent, {
             panelType: PanelType.SLIDE_IN_PANEL,
+            disableClose: true,
             title: 'Save pipeline',
+            width: '40vw',
             data: {
                 pipeline: pipeline,
-                currentModifiedPipelineId: this.currentModifiedPipelineId,
+                originalPipeline: this.originalPipeline,
                 pipelineCanvasMetadata: this.pipelineCanvasMetadata,
             },
         });
+        dialogRef
+            .afterClosed()
+            .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']);
+                    });
+                }
+            });
     }
 
     checkAndDisplayCachedPipeline() {
@@ -280,40 +283,48 @@ export class PipelineAssemblyComponent implements OnInit, 
AfterViewInit {
             if (results[0] && results[0].length > 0) {
                 this.rawPipelineModel = results[0] as PipelineElementConfig[];
                 this.handleCanvasMetadataResponse(results[1]);
+                this.displayPipelineInEditor(
+                    !this.pipelineCanvasMetadataAvailable,
+                    this.pipelineCanvasMetadata,
+                );
             }
         });
     }
 
-    displayPipelineById() {
-        const pipelineRequest = this.pipelineService.getPipelineById(
-            this.currentModifiedPipelineId,
-        );
-        const canvasRequest =
-            this.pipelineCanvasMetadataService.getPipelineCanvasMetadata(
-                this.currentModifiedPipelineId,
-            );
-        pipelineRequest.subscribe(pipelineResp => {
-            const pipeline = pipelineResp;
-            this.currentPipelineName = pipeline.name;
-            this.breadcrumbService.updateBreadcrumb([
-                SpPipelineRoutes.BASE,
-                { label: pipeline.name },
-                { label: 'Modify' },
-            ]);
-            this.currentPipelineDescription = pipeline.description;
-            this.rawPipelineModel = this.jsplumbService.makeRawPipeline(
-                pipeline,
-                false,
-            );
-            canvasRequest.subscribe(
-                canvasResp => {
-                    this.handleCanvasMetadataResponse(canvasResp);
-                },
-                error => {
+    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) {
@@ -324,16 +335,12 @@ export class PipelineAssemblyComponent implements OnInit, 
AfterViewInit {
             this.pipelineCanvasMetadataAvailable = false;
             this.pipelineCanvasMetadata = new PipelineCanvasMetadata();
         }
-        this.displayPipelineInEditor(
-            !this.pipelineCanvasMetadataAvailable,
-            this.pipelineCanvasMetadata,
-        );
     }
 
     displayPipelineInEditor(
-        autoLayout,
+        autoLayout: boolean,
         pipelineCanvasMetadata?: PipelineCanvasMetadata,
-    ) {
+    ): void {
         setTimeout(() => {
             this.pipelinePositioningService.displayPipeline(
                 this.rawPipelineModel,
@@ -409,4 +416,8 @@ export class PipelineAssemblyComponent implements OnInit, 
AfterViewInit {
             },
         });
     }
+
+    ngOnDestroy() {
+        this.moveSub?.unsubscribe();
+    }
 }
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.html
 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.html
new file mode 100644
index 0000000000..56c283b6c0
--- /dev/null
+++ 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.html
@@ -0,0 +1,86 @@
+<!--
+  ~ 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 fxLayout="column">
+    <div
+        id="overwriteCheckbox"
+        class="checkbox"
+        *ngIf="storageOptions.updateModeActive"
+    >
+        <mat-radio-group
+            [(ngModel)]="storageOptions.updateMode"
+            fxLayout="column"
+            color="accent"
+            class="pipeline-radio-group"
+        >
+            <mat-radio-button
+                [value]="'update'"
+                style="padding-left: 0"
+                data-cy="pipeline-update-mode-update"
+            >
+                Update pipeline <b>{{ currentPipelineName }}</b>
+            </mat-radio-button>
+            <mat-radio-button
+                [value]="'clone'"
+                class="mb-10"
+                data-cy="pipeline-update-mode-clone"
+            >
+                Create new pipeline
+            </mat-radio-button>
+        </mat-radio-group>
+    </div>
+    <form [formGroup]="submitPipelineForm">
+        <div
+            fxFlex="100"
+            fxLayout="column"
+            *ngIf="
+                !storageOptions.updateModeActive ||
+                storageOptions.updateMode === 'clone'
+            "
+        >
+            <mat-form-field fxFlex color="accent">
+                <mat-label>Pipeline Name</mat-label>
+                <input
+                    [formControlName]="'pipelineName'"
+                    data-cy="sp-editor-pipeline-name"
+                    matInput
+                    name="pipelineName"
+                    (blur)="triggerTutorial()"
+                />
+            </mat-form-field>
+            <mat-form-field fxFlex color="accent">
+                <mat-label>Description</mat-label>
+                <input [formControlName]="'pipelineDescription'" matInput />
+            </mat-form-field>
+        </div>
+    </form>
+    <mat-checkbox
+        [(ngModel)]="storageOptions.startPipelineAfterStorage"
+        color="accent"
+        data-cy="sp-editor-checkbox-start-immediately"
+    >
+        Start pipeline immediately
+    </mat-checkbox>
+    <mat-checkbox
+        [(ngModel)]="storageOptions.navigateToPipelineOverview"
+        color="accent"
+        data-cy="sp-editor-checkbox-navigate-to-overview"
+    >
+        Navigate to pipeline overview afterwards
+    </mat-checkbox>
+</div>
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.scss
similarity index 68%
copy from ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
copy to 
ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.scss
index 5b19b82153..13cbc4aacb 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
+++ 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.scss
@@ -15,32 +15,3 @@
  * limitations under the License.
  *
  */
-
-@import '../../../../scss/sp/sp-dialog.scss';
-
-.customize-section {
-    display: flex;
-    flex: 1 1 auto;
-    padding: 20px;
-}
-
-.padding-20 {
-    padding: 20px;
-}
-
-.mb-10 {
-    margin-bottom: 10px;
-}
-
-::ng-deep .pipeline-radio-group .mat-radio-label {
-    padding: 0;
-}
-
-.status-text {
-    font-size: 14pt;
-    margin-top: 10px;
-}
-
-.status-subtext {
-    font-size: 12pt;
-}
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.ts
 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.ts
new file mode 100644
index 0000000000..6f32859192
--- /dev/null
+++ 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.ts
@@ -0,0 +1,82 @@
+/*
+ * 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, OnInit } from '@angular/core';
+import { ShepherdService } from '../../../../services/tour/shepherd.service';
+import {
+    UntypedFormControl,
+    UntypedFormGroup,
+    Validators,
+} from '@angular/forms';
+import { Pipeline } from '@streampipes/platform-services';
+import { PipelineStorageOptions } from '../../../model/editor.model';
+
+@Component({
+    selector: 'sp-save-pipeline-settings',
+    templateUrl: './save-pipeline-settings.component.html',
+    styleUrls: ['./save-pipeline-settings.component.scss'],
+})
+export class SavePipelineSettingsComponent implements OnInit {
+    @Input()
+    submitPipelineForm: UntypedFormGroup = new UntypedFormGroup({});
+
+    @Input()
+    pipeline: Pipeline;
+
+    @Input()
+    storageOptions: PipelineStorageOptions;
+
+    @Input()
+    currentPipelineName: string;
+
+    constructor(private shepherdService: ShepherdService) {}
+
+    ngOnInit() {
+        this.submitPipelineForm.addControl(
+            'pipelineName',
+            new UntypedFormControl(this.pipeline.name, [
+                Validators.required,
+                Validators.maxLength(40),
+            ]),
+        );
+        this.submitPipelineForm.addControl(
+            'pipelineDescription',
+            new UntypedFormControl(this.pipeline.description, [
+                Validators.maxLength(80),
+            ]),
+        );
+
+        
this.submitPipelineForm.controls['pipelineName'].valueChanges.subscribe(
+            value => {
+                this.pipeline.name = value;
+            },
+        );
+
+        this.submitPipelineForm.controls[
+            'pipelineDescription'
+        ].valueChanges.subscribe(value => {
+            this.pipeline.description = value;
+        });
+    }
+
+    triggerTutorial() {
+        if (this.shepherdService.isTourActive()) {
+            this.shepherdService.trigger('save-pipeline-dialog');
+        }
+    }
+}
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html
index 94af6d9021..9d5bf72304 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html
+++ b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html
@@ -19,138 +19,78 @@
 <div class="sp-dialog-container">
     <div class="sp-dialog-content padding-20">
         <div fxFlex="100" fxLayout="column">
-            <div
-                fxFlex="100"
-                fxLayout="column"
-                *ngIf="!saved && !saving && !storageError"
+            <sp-save-pipeline-settings
+                *ngIf="!operationCompleted && !operationProgress"
+                [currentPipelineName]="pipeline.name"
+                [submitPipelineForm]="submitPipelineForm"
+                [pipeline]="pipeline"
+                [storageOptions]="storageOptions"
             >
-                <div
-                    id="overwriteCheckbox"
-                    class="checkbox"
-                    *ngIf="currentModifiedPipelineId"
-                >
-                    <mat-radio-group
-                        [(ngModel)]="updateMode"
-                        fxLayout="column"
-                        color="accent"
-                        class="pipeline-radio-group"
-                    >
-                        <mat-radio-button
-                            [value]="'update'"
-                            class="mb-10"
-                            style="padding-left: 0"
-                        >
-                            Update pipeline <b>{{ currentPipelineName }}</b>
-                        </mat-radio-button>
-                        <mat-radio-button [value]="'clone'" class="mb-10">
-                            Create new pipeline
-                        </mat-radio-button>
-                    </mat-radio-group>
-                </div>
-                <form [formGroup]="submitPipelineForm">
-                    <div
-                        fxFlex="100"
-                        fxLayout="column"
-                        *ngIf="
-                            !currentModifiedPipelineId || updateMode === 
'clone'
-                        "
-                    >
-                        <mat-form-field fxFlex color="accent">
-                            <mat-label>Pipeline Name</mat-label>
-                            <input
-                                [formControlName]="'pipelineName'"
-                                data-cy="sp-editor-pipeline-name"
-                                matInput
-                                name="pipelineName"
-                            />
-                        </mat-form-field>
-                        <mat-form-field fxFlex color="accent">
-                            <mat-label>Description</mat-label>
-                            <input
-                                [formControlName]="'pipelineDescription'"
-                                matInput
-                            />
-                        </mat-form-field>
-                    </div>
-                </form>
-                <mat-checkbox
-                    (click)="triggerTutorial()"
-                    [(ngModel)]="startPipelineAfterStorage"
-                    color="accent"
-                    data-cy="sp-editor-checkbox-start-immediately"
-                >
-                    Start pipeline immediately
-                </mat-checkbox>
-            </div>
-            <div
-                fxFlex="100"
-                fxLayout="column"
-                fxLayoutAlign="center center"
-                *ngIf="saving"
-            >
-                <mat-spinner
-                    [mode]="'indeterminate'"
-                    [diameter]="50"
-                    color="accent"
-                ></mat-spinner>
-                <span class="status-text">Saving pipeline...</span>
-            </div>
-            <div
-                fxFlex="100"
-                fxLayout="column"
-                fxLayoutAlign="center center"
-                *ngIf="saved"
-            >
-                <mat-icon
-                    color="accent"
-                    style="font-size: 50pt; height: 60px; width: 60px"
-                    >check_circle</mat-icon
-                >
-                <span class="status-text">Pipeline successfully stored.</span>
-            </div>
-            <div
-                fxFlex="100"
-                fxLayout="column"
-                fxLayoutAlign="center center"
-                *ngIf="storageError"
+            </sp-save-pipeline-settings>
+
+            <sp-multi-step-status-indicator
+                *ngIf="operationProgress || operationCompleted"
+                [statusIndicators]="statusIndicators"
             >
-                <mat-icon
-                    color="accent"
-                    style="font-size: 50pt; height: 60px; width: 60px"
-                    >error</mat-icon
-                >
-                <span class="status-text"
-                    >Your pipeline could not be stored.</span
+            </sp-multi-step-status-indicator>
+
+            <div *ngIf="finalPipelineOperationStatus" class="mt-10">
+                <mat-divider></mat-divider>
+                <sp-pipeline-started-status
+                    class="mt-10"
+                    [forceStopDisabled]="true"
+                    [action]="pipelineAction"
+                    [pipelineOperationStatus]="finalPipelineOperationStatus"
                 >
-                <span class="status-subtext">{{ errorMessage }}</span>
+                </sp-pipeline-started-status>
             </div>
         </div>
     </div>
     <mat-divider></mat-divider>
     <div class="sp-dialog-actions">
         <button
-            [disabled]="!submitPipelineForm.valid || saving || saved"
-            mat-button
             mat-raised-button
             color="accent"
-            (click)="savePipeline(false)"
-            style="margin-right: 10px"
+            (click)="hide(false)"
+            *ngIf="operationCompleted"
         >
-            Save
+            Create another pipeline
         </button>
         <button
-            [disabled]="!submitPipelineForm.valid || saving || saved"
+            mat-raised-button
+            color="accent"
+            data-cy="sp-navigate-to-pipeline-overview"
+            (click)="navigateToPipelineOverview()"
+            *ngIf="operationCompleted"
+        >
+            Open pipeline overview
+        </button>
+        <button
+            *ngIf="!operationCompleted && !operationSuccess"
+            [disabled]="
+                !submitPipelineForm.valid ||
+                operationProgress ||
+                operationCompleted
+            "
             mat-button
             mat-raised-button
             color="accent"
-            (click)="savePipeline(true)"
+            (click)="savePipeline()"
             style="margin-right: 10px"
-            data-cy="sp-editor-save"
+            data-cy="sp-editor-apply"
         >
-            Save and go to pipeline view
+            Apply
         </button>
-        <button mat-button mat-raised-button class="mat-basic" 
(click)="hide()">
-            {{ saved ? 'Close' : 'Cancel' }}
+        <button
+            *ngIf="
+                !operationProgress && (!operationCompleted || 
!operationSuccess)
+            "
+            mat-button
+            mat-raised-button
+            class="mat-basic"
+            (click)="hide(true)"
+        >
+            {{ 'Cancel' }}
         </button>
     </div>
 </div>
diff --git 
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
index 5b19b82153..32ffbd0a6d 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
+++ b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
@@ -35,12 +35,3 @@
 ::ng-deep .pipeline-radio-group .mat-radio-label {
     padding: 0;
 }
-
-.status-text {
-    font-size: 14pt;
-    margin-top: 10px;
-}
-
-.status-subtext {
-    font-size: 12pt;
-}
diff --git a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts 
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts
index 863d9693fc..7efa549cf1 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts
+++ b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts
@@ -23,18 +23,25 @@ import {
     Pipeline,
     PipelineCanvasMetadata,
     PipelineCanvasMetadataService,
+    PipelineOperationStatus,
     PipelineService,
 } from '@streampipes/platform-services';
 import { EditorService } from '../../services/editor.service';
 import { ShepherdService } from '../../../services/tour/shepherd.service';
-import {
-    UntypedFormControl,
-    UntypedFormGroup,
-    Validators,
-} from '@angular/forms';
+import { UntypedFormGroup } from '@angular/forms';
 import { Router } from '@angular/router';
-import { InvocablePipelineElementUnion } from '../../model/editor.model';
+import {
+    InvocablePipelineElementUnion,
+    PipelineStorageOptions,
+} from '../../model/editor.model';
 import { IdGeneratorService } from 
'../../../core-services/id-generator/id-generator.service';
+import { Observable, of, tap } from 'rxjs';
+import { filter, switchMap } from 'rxjs/operators';
+import {
+    Status,
+    StatusIndicator,
+} from 
'../../../core-ui/multi-step-status-indicator/multi-step-status-indicator.model';
+import { PipelineAction } from '../../../pipelines/model/pipeline-model';
 
 @Component({
     selector: 'sp-save-pipeline',
@@ -42,30 +49,33 @@ import { IdGeneratorService } from 
'../../../core-services/id-generator/id-gener
     styleUrls: ['./save-pipeline.component.scss'],
 })
 export class SavePipelineComponent implements OnInit {
-    startPipelineAfterStorage: boolean;
-    updateMode: any;
-
-    submitPipelineForm: UntypedFormGroup = new UntypedFormGroup({});
-
     @Input()
     pipeline: Pipeline;
 
     @Input()
-    modificationMode: string;
-
-    @Input()
-    currentModifiedPipelineId: string;
+    originalPipeline: Pipeline;
 
     @Input()
     pipelineCanvasMetadata: PipelineCanvasMetadata;
 
-    saving = false;
-    saved = false;
+    operationProgress = false;
+    operationCompleted = false;
+    operationSuccess = false;
 
-    storageError = false;
     errorMessage = '';
+    pipelineId: string;
 
-    currentPipelineName: string;
+    storageOptions: PipelineStorageOptions = {
+        updateMode: 'update',
+        startPipelineAfterStorage: true,
+        navigateToPipelineOverview: true,
+        updateModeActive: false,
+    };
+
+    submitPipelineForm: UntypedFormGroup = new UntypedFormGroup({});
+    statusIndicators: StatusIndicator[] = [];
+    finalPipelineOperationStatus: PipelineOperationStatus;
+    pipelineAction: PipelineAction;
 
     constructor(
         private editorService: EditorService,
@@ -75,90 +85,95 @@ export class SavePipelineComponent implements OnInit {
         private router: Router,
         private shepherdService: ShepherdService,
         private pipelineCanvasService: PipelineCanvasMetadataService,
-    ) {
-        this.updateMode = 'update';
-    }
+    ) {}
 
     ngOnInit() {
-        if (this.currentModifiedPipelineId) {
-            this.currentPipelineName = this.pipeline.name;
+        this.storageOptions.updateModeActive =
+            this.originalPipeline !== undefined;
+        if (this.storageOptions.updateModeActive) {
+            this.pipeline._id = this.originalPipeline._id;
+            this.pipeline.name = this.originalPipeline.name;
+            this.pipeline.description = this.originalPipeline.description;
+            this.pipeline.running = this.originalPipeline.running;
+            this.pipeline.createdAt = this.originalPipeline.createdAt;
+            this.pipeline.createdByUser = this.originalPipeline.createdByUser;
         }
 
-        this.submitPipelineForm.addControl(
-            'pipelineName',
-            new UntypedFormControl(this.pipeline.name, [
-                Validators.required,
-                Validators.maxLength(40),
-            ]),
-        );
-        this.submitPipelineForm.addControl(
-            'pipelineDescription',
-            new UntypedFormControl(this.pipeline.description, [
-                Validators.maxLength(80),
-            ]),
-        );
-
-        
this.submitPipelineForm.controls['pipelineName'].valueChanges.subscribe(
-            value => {
-                this.pipeline.name = value;
-            },
-        );
-
-        this.submitPipelineForm.controls[
-            'pipelineDescription'
-        ].valueChanges.subscribe(value => {
-            this.pipeline.description = value;
-        });
-
         if (this.shepherdService.isTourActive()) {
             this.shepherdService.trigger('enter-pipeline-name');
         }
     }
 
-    triggerTutorial() {
-        if (this.shepherdService.isTourActive()) {
-            this.shepherdService.trigger('save-pipeline-dialog');
-        }
+    performStorageOperations(
+        stopPipeline$: Observable<null | PipelineOperationStatus>,
+        savePipeline$: Observable<Message>,
+    ) {
+        // if pipeline is running and update mode: stop pipeline
+        // if update mode: update pipeline, if not update mode or update mode 
clone: save pipeline
+        // if update mode and not clone: update canvas, else store new canvas
+        // if should start: start pipeline
+        stopPipeline$
+            .pipe(
+                tap(() =>
+                    this.addStatusIndicator('Saving pipeline', 
Status.PROGRESS),
+                ),
+                switchMap(() => savePipeline$),
+                tap(message => {
+                    this.operationSuccess = message.success;
+                    if (!message.success) {
+                        this.handleStorageError();
+                    }
+                    this.modifyStatusIndicator(Status.SUCCESS);
+                    this.pipelineId = message.notifications[1].description;
+                }),
+                // only continue if pipeline was saved
+                filter(message => message.success),
+                tap(() =>
+                    this.addStatusIndicator('Saving metadata', 
Status.PROGRESS),
+                ),
+                switchMap(() =>
+                    this.getPipelineCanvasMetadata$(this.pipelineId),
+                ),
+                tap(() => this.modifyStatusIndicator(Status.SUCCESS)),
+                switchMap(() => this.getStartPipeline$()),
+            )
+            .subscribe({
+                next: message => this.onSuccess(message),
+                error: msg => {
+                    this.onFailure(msg);
+                },
+            });
     }
 
-    displayErrors(data?: string) {
-        this.storageError = true;
-        this.errorMessage = data;
+    clonePipeline(): void {
+        this.pipeline._id = undefined;
+        this.pipeline._rev = undefined;
+        this.pipeline.running = false;
+        this.pipeline.actions.forEach(element => this.updateId(element));
+        this.pipeline.sepas.forEach(element => this.updateId(element));
+        this.pipelineCanvasMetadata._id = undefined;
+        this.pipelineCanvasMetadata._rev = undefined;
     }
 
-    savePipeline(switchTab) {
-        let storageRequest;
-        const updateMode =
-            this.currentModifiedPipelineId && this.updateMode === 'update';
-
-        if (updateMode) {
-            storageRequest = 
this.pipelineService.updatePipeline(this.pipeline);
-        } else {
-            if (this.currentModifiedPipelineId) {
-                this.pipeline.actions.forEach(element =>
-                    this.updateId(element),
+    savePipeline() {
+        let stopPipeline$: Observable<null | PipelineOperationStatus> =
+            of(null);
+        let savePipeline$: Observable<Message> =
+            this.pipelineService.storePipeline(this.pipeline);
+        this.operationProgress = true;
+        if (this.storageOptions.updateModeActive) {
+            if (this.storageOptions.updateMode === 'clone') {
+                this.clonePipeline();
+            } else {
+                if (this.pipeline.running) {
+                    stopPipeline$ = this.getStopPipeline$();
+                }
+                savePipeline$ = this.pipelineService.updatePipeline(
+                    this.pipeline,
                 );
-                this.pipeline.sepas.forEach(element => this.updateId(element));
             }
-            this.pipeline._id = undefined;
-            storageRequest = this.pipelineService.storePipeline(this.pipeline);
         }
-
-        storageRequest.subscribe(
-            statusMessage => {
-                if (statusMessage.success) {
-                    const pipelineId: string =
-                        statusMessage.notifications[1].description;
-                    this.storePipelineCanvasMetadata(pipelineId, updateMode);
-                    this.afterStorage(statusMessage, switchTab, pipelineId);
-                } else {
-                    this.displayErrors(statusMessage.notifications[0]);
-                }
-            },
-            data => {
-                this.displayErrors();
-            },
-        );
+        this.performStorageOperations(stopPipeline$, savePipeline$);
     }
 
     updateId(entity: InvocablePipelineElementUnion) {
@@ -168,10 +183,60 @@ export class SavePipelineComponent implements OnInit {
             this.idGeneratorService.generate(5);
     }
 
-    storePipelineCanvasMetadata(pipelineId: string, updateMode: boolean) {
+    getStopPipeline$(): Observable<PipelineOperationStatus> {
+        return of(null).pipe(
+            tap(() =>
+                this.addStatusIndicator('Stopping pipeline', Status.PROGRESS),
+            ),
+            switchMap(() =>
+                this.pipelineService.stopPipeline(this.originalPipeline._id),
+            ),
+            tap(msg => {
+                this.operationSuccess = msg.success;
+                if (!msg.success) {
+                    this.handlePipelineOperationError(msg, 
PipelineAction.Stop);
+                } else {
+                    this.modifyStatusIndicator(Status.SUCCESS);
+                }
+            }),
+            filter(status => status.success),
+        );
+    }
+
+    getStartPipeline$(): Observable<null | PipelineOperationStatus> {
+        if (this.storageOptions.startPipelineAfterStorage) {
+            return of(null).pipe(
+                tap(() =>
+                    this.addStatusIndicator(
+                        'Starting pipeline',
+                        Status.PROGRESS,
+                    ),
+                ),
+                switchMap(() =>
+                    this.pipelineService.startPipeline(this.pipelineId),
+                ),
+                tap(msg => {
+                    if (!msg.success) {
+                        this.handlePipelineOperationError(
+                            msg,
+                            PipelineAction.Start,
+                        );
+                    } else {
+                        this.modifyStatusIndicator(
+                            msg.success ? Status.SUCCESS : Status.FAILURE,
+                        );
+                    }
+                }),
+            );
+        } else {
+            return of(null);
+        }
+    }
+
+    getPipelineCanvasMetadata$(pipelineId: string): Observable<Object> {
         let request;
         this.pipelineCanvasMetadata.pipelineId = pipelineId;
-        if (updateMode) {
+        if (this.storageOptions.updateModeActive) {
             request = this.pipelineCanvasService.updatePipelineCanvasMetadata(
                 this.pipelineCanvasMetadata,
             );
@@ -182,28 +247,72 @@ export class SavePipelineComponent implements OnInit {
                 this.pipelineCanvasMetadata,
             );
         }
+        return request;
+    }
+
+    addStatusIndicator(message: string, status: Status) {
+        this.statusIndicators.push({ message, status });
+    }
+
+    modifyStatusIndicator(status: Status) {
+        // modify status of the last indicator
+        this.statusIndicators[this.statusIndicators.length - 1].status = 
status;
+    }
 
-        request.subscribe();
+    handleStorageError(): void {
+        this.onFailure();
     }
 
-    afterStorage(statusMessage: Message, switchTab, pipelineId?: string) {
-        this.hide();
+    handlePipelineOperationError(
+        status: PipelineOperationStatus,
+        pipelineAction: PipelineAction,
+    ) {
+        this.onFailure();
+        this.showPipelineOperationStatus(status, pipelineAction);
+    }
+
+    onFailure(msg?: any) {
+        this.operationCompleted = true;
+        this.operationSuccess = false;
+        this.modifyStatusIndicator(Status.FAILURE);
+    }
+
+    showPipelineOperationStatus(
+        status: PipelineOperationStatus,
+        pipelineAction: PipelineAction,
+    ) {
+        this.finalPipelineOperationStatus = status;
+        this.pipelineAction = pipelineAction;
+    }
+
+    onSuccess(status?: PipelineOperationStatus) {
+        this.operationProgress = false;
+        this.operationCompleted = true;
+        if (status) {
+            this.showPipelineOperationStatus(status, PipelineAction.Start);
+        }
         this.editorService.makePipelineAssemblyEmpty(true);
         this.editorService.removePipelineFromCache().subscribe();
         if (this.shepherdService.isTourActive()) {
             this.shepherdService.hideCurrentStep();
         }
-        if (switchTab && !this.startPipelineAfterStorage) {
-            this.router.navigate(['pipelines']);
-        }
-        if (this.startPipelineAfterStorage) {
-            this.router.navigate(['pipelines'], {
-                queryParams: { pipeline: pipelineId },
-            });
+        if (this.storageOptions.navigateToPipelineOverview) {
+            this.navigateToPipelineOverview();
         }
     }
 
-    hide() {
-        this.dialogRef.close();
+    navigateToPipelineOverview(): void {
+        this.hide(true);
+        this.router.navigate(['pipelines']);
+    }
+
+    hide(skipReload: boolean) {
+        let reloadConfig = undefined;
+        if (!skipReload) {
+            reloadConfig = this.operationSuccess
+                ? { reload: true, pipelineId: this.pipelineId }
+                : undefined;
+        }
+        this.dialogRef.close(reloadConfig);
     }
 }
diff --git a/ui/src/app/editor/editor.component.html 
b/ui/src/app/editor/editor.component.html
index fae4332d0e..27da459f4a 100644
--- a/ui/src/app/editor/editor.component.html
+++ b/ui/src/app/editor/editor.component.html
@@ -18,25 +18,25 @@
 
 <div fxLayout="column" class="page-container">
     <div
-        class="fixed-height"
+        fxFlex="100"
+        fxLayout="column"
+        fxLayoutAlign="center center"
+        *ngIf="!allElementsLoaded"
+    >
+        <mat-spinner
+            [diameter]="30"
+            color="accent"
+            mode="indeterminate"
+        ></mat-spinner>
+        <p>Preparing pipeline editor...</p>
+    </div>
+    <div
+        *ngIf="allElementsLoaded"
+        class="fixed-height editor-container-inner"
         fxLayout="row"
         fxFlex="100"
-        class="editor-container-inner"
     >
         <div fxFlex="250px">
-            <div
-                fxFlex="100"
-                fxLayout="column"
-                fxLayoutAlign="center center"
-                *ngIf="!allElementsLoaded"
-            >
-                <mat-spinner
-                    [diameter]="30"
-                    color="accent"
-                    mode="indeterminate"
-                ></mat-spinner>
-                <p>Loading pipeline elements...</p>
-            </div>
             <div
                 id="shepherd-test"
                 style="
diff --git a/ui/src/app/editor/editor.component.ts 
b/ui/src/app/editor/editor.component.ts
index 59a7bfacf3..a6ed4a5a0b 100644
--- a/ui/src/app/editor/editor.component.ts
+++ b/ui/src/app/editor/editor.component.ts
@@ -47,16 +47,15 @@ export class EditorComponent implements OnInit {
     ) {}
 
     ngOnInit() {
-        this.activatedRoute.params.subscribe(params => {
-            if (params.pipelineId) {
-                this.currentModifiedPipelineId = params.pipelineId;
-            } else {
-                this.breadcrumbService.updateBreadcrumb([
-                    SpPipelineRoutes.BASE,
-                    { label: 'New Pipeline' },
-                ]);
-            }
-        });
+        const pipelineId = this.activatedRoute.snapshot.params.pipelineId;
+        if (pipelineId) {
+            this.currentModifiedPipelineId = pipelineId;
+        } else {
+            this.breadcrumbService.updateBreadcrumb([
+                SpPipelineRoutes.BASE,
+                { label: 'New Pipeline' },
+            ]);
+        }
         zip(
             this.pipelineElementService.getDataStreams(),
             this.pipelineElementService.getDataProcessors(),
diff --git a/ui/src/app/editor/editor.module.ts 
b/ui/src/app/editor/editor.module.ts
index 8e647b451a..f1d0090ee2 100644
--- a/ui/src/app/editor/editor.module.ts
+++ b/ui/src/app/editor/editor.module.ts
@@ -74,6 +74,7 @@ import { MatProgressBarModule } from 
'@angular/material/progress-bar';
 import { MatButtonToggleModule } from '@angular/material/button-toggle';
 import { MatChipsModule } from '@angular/material/chips';
 import { MatSliderModule } from '@angular/material/slider';
+import { SavePipelineSettingsComponent } from 
'./dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component';
 
 @NgModule({
     imports: [
@@ -138,6 +139,7 @@ import { MatSliderModule } from '@angular/material/slider';
         PipelineComponent,
         PropertySelectionComponent,
         SavePipelineComponent,
+        SavePipelineSettingsComponent,
         SafeCss,
     ],
     providers: [SafeCss],
diff --git a/ui/src/app/editor/model/editor.model.ts 
b/ui/src/app/editor/model/editor.model.ts
index c92142950c..54c2ba151e 100644
--- a/ui/src/app/editor/model/editor.model.ts
+++ b/ui/src/app/editor/model/editor.model.ts
@@ -21,10 +21,12 @@ import {
     DataSinkInvocation,
     SpDataStream,
 } from '@streampipes/platform-services';
-import { InjectionToken } from '@angular/core';
 
-export interface PipelineElementHolder {
-    [key: string]: PipelineElementUnion[];
+export interface PipelineStorageOptions {
+    startPipelineAfterStorage: boolean;
+    navigateToPipelineOverview: boolean;
+    updateModeActive: boolean;
+    updateMode: 'update' | 'clone';
 }
 
 export interface PipelineElementPosition {
@@ -55,25 +57,12 @@ export interface PipelineElementConfig {
     payload: PipelineElementUnion;
 }
 
-export interface PipelineElementRecommendationLayout {
-    skewStyle: any;
-    unskewStyle: any;
-    unskewStyleLabel: any;
-    type: string;
-}
-
 export enum PipelineElementType {
     DataStream,
     DataProcessor,
     DataSink,
 }
 
-export interface TabsModel {
-    title: string;
-    type: PipelineElementIdentifier;
-    shorthand: string;
-}
-
 export type PipelineElementUnion =
     | SpDataStream
     | DataProcessorInvocation
@@ -83,8 +72,6 @@ export type InvocablePipelineElementUnion =
     | DataProcessorInvocation
     | DataSinkInvocation;
 
-export const PIPELINE_ELEMENT_TOKEN = new 
InjectionToken<{}>('pipelineElement');
-
 export type PipelineElementIdentifier =
     | 'org.apache.streampipes.model.SpDataStream'
     | 'org.apache.streampipes.model.graph.DataProcessorInvocation'
diff --git 
a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
 
b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
index 0c262d080d..924a9e1004 100644
--- 
a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
+++ 
b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
@@ -135,9 +135,9 @@
         <th mat-header-cell mat-sort-header *matHeaderCellDef>Name</th>
         <td mat-cell *matCellDef="let pipeline">
             <h4 style="margin-bottom: 0">{{ pipeline.name }}</h4>
-            <h5>
+            <span>
                 {{ pipeline.description !== '' ? pipeline.description : '-' }}
-            </h5>
+            </span>
         </td>
     </ng-container>
 
@@ -170,7 +170,6 @@
                     mat-icon-button
                     matTooltip="Modify pipeline"
                     matTooltipPosition="above"
-                    [disabled]="pipeline.running"
                     *ngIf="hasPipelineWritePrivileges"
                     (click)="
                         pipelineOperationsService.modifyPipeline(pipeline._id)
diff --git 
a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts
 
b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts
index 3519316ccb..aa78ffac1c 100644
--- 
a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts
+++ 
b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts
@@ -46,9 +46,6 @@ import { Subscription } from 'rxjs';
 export class PipelineOverviewComponent implements OnInit, OnDestroy {
     _pipelines: Pipeline[];
 
-    @Input()
-    pipelineToStart: Pipeline;
-
     @Output()
     refreshPipelinesEmitter: EventEmitter<boolean> =
         new EventEmitter<boolean>();
@@ -93,16 +90,6 @@ export class PipelineOverviewComponent implements OnInit, 
OnDestroy {
             );
         });
         this.toggleRunningOperation = this.toggleRunningOperation.bind(this);
-
-        if (this.pipelineToStart) {
-            if (!this.pipelineToStart.running) {
-                this.pipelineOperationsService.startPipeline(
-                    this.pipelineToStart._id,
-                    this.refreshPipelinesEmitter,
-                    this.toggleRunningOperation,
-                );
-            }
-        }
     }
 
     toggleRunningOperation(currentOperation: string) {
diff --git 
a/ui/src/app/pipelines/dialog/pipeline-status/pipeline-status-dialog.component.ts
 
b/ui/src/app/pipelines/dialog/pipeline-status/pipeline-status-dialog.component.ts
index c60548217f..31c411bdbf 100644
--- 
a/ui/src/app/pipelines/dialog/pipeline-status/pipeline-status-dialog.component.ts
+++ 
b/ui/src/app/pipelines/dialog/pipeline-status/pipeline-status-dialog.component.ts
@@ -23,7 +23,6 @@ import {
 } from '@streampipes/platform-services';
 import { Component, Input, OnInit } from '@angular/core';
 import { PipelineAction } from '../../model/pipeline-model';
-import { ShepherdService } from '../../../services/tour/shepherd.service';
 
 @Component({
     selector: 'sp-pipeline-status-dialog',
@@ -44,7 +43,6 @@ export class PipelineStatusDialogComponent implements OnInit {
     constructor(
         private dialogRef: DialogRef<PipelineStatusDialogComponent>,
         private pipelineService: PipelineService,
-        private shepherdService: ShepherdService,
     ) {}
 
     ngOnInit(): void {
@@ -64,9 +62,6 @@ export class PipelineStatusDialogComponent implements OnInit {
             msg => {
                 this.pipelineOperationStatus = msg;
                 this.operationInProgress = false;
-                if (this.shepherdService.isTourActive()) {
-                    this.shepherdService.trigger('pipeline-started');
-                }
             },
             error => {
                 this.operationInProgress = false;
diff --git a/ui/src/app/pipelines/pipelines.component.html 
b/ui/src/app/pipelines/pipelines.component.html
index fa7fc442f4..73338ecaba 100644
--- a/ui/src/app/pipelines/pipelines.component.html
+++ b/ui/src/app/pipelines/pipelines.component.html
@@ -83,7 +83,6 @@
                 <div fxFlex="90">
                     <sp-pipeline-overview
                         [pipelines]="pipelines"
-                        [pipelineToStart]="pipelineToStart"
                         (refreshPipelinesEmitter)="refreshPipelines()"
                         *ngIf="pipelinesReady"
                     ></sp-pipeline-overview>
diff --git a/ui/src/app/pipelines/pipelines.component.ts 
b/ui/src/app/pipelines/pipelines.component.ts
index 55f13b3927..1a4ef6da47 100644
--- a/ui/src/app/pipelines/pipelines.component.ts
+++ b/ui/src/app/pipelines/pipelines.component.ts
@@ -31,7 +31,7 @@ import {
     SpBreadcrumbService,
 } from '@streampipes/shared-ui';
 import { StartAllPipelinesDialogComponent } from 
'./dialog/start-all-pipelines/start-all-pipelines-dialog.component';
-import { ActivatedRoute, Router } from '@angular/router';
+import { Router } from '@angular/router';
 import { AuthService } from '../services/auth.service';
 import { UserPrivilege } from '../_enums/user-privilege.enum';
 import { SpPipelineRoutes } from './pipelines.routes';
@@ -50,9 +50,6 @@ export class PipelinesComponent implements OnInit, OnDestroy {
     starting: boolean;
     stopping: boolean;
 
-    pipelineIdToStart: string;
-    pipelineToStart: Pipeline;
-
     pipelinesReady = false;
     hasPipelineWritePrivileges = false;
 
@@ -61,15 +58,12 @@ export class PipelinesComponent implements OnInit, 
OnDestroy {
     isAdminRole = false;
 
     tutorialActive = false;
-
-    activatedRouteSubscription: Subscription;
     tutorialActiveSubscription: Subscription;
     userSubscription: Subscription;
 
     constructor(
         private pipelineService: PipelineService,
         private dialogService: DialogService,
-        private activatedRoute: ActivatedRoute,
         private authService: AuthService,
         private currentUserService: CurrentUserService,
         private router: Router,
@@ -95,17 +89,11 @@ export class PipelinesComponent implements OnInit, 
OnDestroy {
                 );
             },
         );
-        this.activatedRouteSubscription =
-            this.activatedRoute.queryParams.subscribe(params => {
-                if (params['pipeline']) {
-                    this.pipelineIdToStart = params['pipeline'];
-                }
-                if (params.startTutorial) {
-                    this.startPipelineTour();
-                }
-                this.getPipelines();
-                this.getFunctions();
-            });
+        if (this.shepherdService.isTourActive()) {
+            this.shepherdService.trigger('pipeline-started');
+        }
+        this.getPipelines();
+        this.getFunctions();
         this.tutorialActiveSubscription =
             this.shepherdService.tutorialActive$.subscribe(tutorialActive => {
                 this.tutorialActive = tutorialActive;
@@ -123,21 +111,10 @@ export class PipelinesComponent implements OnInit, 
OnDestroy {
         this.pipelines = [];
         this.pipelineService.getPipelines().subscribe(pipelines => {
             this.pipelines = pipelines;
-            this.checkForImmediateStart(pipelines);
             this.pipelinesReady = true;
         });
     }
 
-    checkForImmediateStart(pipelines: Pipeline[]) {
-        this.pipelineToStart = undefined;
-        pipelines.forEach(pipeline => {
-            if (pipeline._id === this.pipelineIdToStart) {
-                this.pipelineToStart = pipeline;
-            }
-        });
-        this.pipelineIdToStart = undefined;
-    }
-
     checkCurrentSelectionStatus(status) {
         let active = true;
         this.pipelines.forEach(pipeline => {
@@ -184,7 +161,6 @@ export class PipelinesComponent implements OnInit, 
OnDestroy {
     }
 
     ngOnDestroy() {
-        this.activatedRouteSubscription?.unsubscribe();
         this.userSubscription?.unsubscribe();
         this.tutorialActiveSubscription?.unsubscribe();
     }
diff --git a/ui/src/app/services/tour/create-pipeline-tour.constants.ts 
b/ui/src/app/services/tour/create-pipeline-tour.constants.ts
index c99c9f5d69..39209c18a1 100644
--- a/ui/src/app/services/tour/create-pipeline-tour.constants.ts
+++ b/ui/src/app/services/tour/create-pipeline-tour.constants.ts
@@ -136,7 +136,7 @@ export default {
             {
                 stepId: 'step-15',
                 title: 'Save Pipeline Dialog',
-                text: '<p>Click on <b>Save and go to pipeline view</b> to 
start the pipeline.</p>',
+                text: '<p>Click on <b>Apply</b> to start the pipeline.</p>',
                 attachToElement: '[data-cy="sp-editor-save"]',
                 attachPosition: 'top',
                 buttons: ['cancel'],
diff --git a/ui/src/app/services/tour/shepherd.service.ts 
b/ui/src/app/services/tour/shepherd.service.ts
index 069c33f5d8..bab7b97155 100644
--- a/ui/src/app/services/tour/shepherd.service.ts
+++ b/ui/src/app/services/tour/shepherd.service.ts
@@ -206,14 +206,6 @@ export class ShepherdService {
         this.startTour(this.tourProviderService.getTourById('adapter'));
     }
 
-    setTimeWaitMillis(value) {
-        this.tourProviderService.setTime(value);
-    }
-
-    getTimeWaitMillis() {
-        return this.tourProviderService.getTime();
-    }
-
     changeTutorialStatus(tutorialActive: boolean): void {
         this.tutorialActive = tutorialActive;
         this.tutorialActive$.next(tutorialActive);
diff --git a/ui/src/scss/sp/main.scss b/ui/src/scss/sp/main.scss
index f1c21f0d0d..269701f473 100644
--- a/ui/src/scss/sp/main.scss
+++ b/ui/src/scss/sp/main.scss
@@ -861,10 +861,10 @@ label {
 .error-message {
     background-color: black;
     font:
-        0.8rem Inconsolata,
+        0.7rem Inconsolata,
         monospace;
     text-shadow: 0 0 5px #c8c8c8;
     color: white;
-    padding: 10px;
     width: 100%;
+    max-width: 100%;
 }
diff --git a/ui/src/scss/sp/shepherd-new.scss b/ui/src/scss/sp/shepherd-new.scss
index 6da36608a8..2615e66f72 100644
--- a/ui/src/scss/sp/shepherd-new.scss
+++ b/ui/src/scss/sp/shepherd-new.scss
@@ -54,6 +54,7 @@
     display: flex;
     justify-content: flex-end;
     padding: 0 0.75rem 0.75rem;
+    background: white;
 }
 
 .shepherd-footer .shepherd-button:last-child {
@@ -114,6 +115,7 @@
     font-size: 1rem;
     line-height: 1.3em;
     padding: 0.75em;
+    background: white;
 }
 
 .shepherd-text p {
@@ -168,11 +170,10 @@
     height: 16px;
     position: absolute;
     width: 16px;
-    z-index: -1;
 }
 
 .shepherd-arrow:before {
-    background: #fafafa;
+    background: lightblue;
     content: '';
     transform: rotate(45deg);
 }


Reply via email to