You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ri...@apache.org on 2020/06/12 08:22:14 UTC

[incubator-streampipes] 02/02: [STREAMPIPES-145] Migrate pipeline start dialog

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

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

commit d2c63f9b96728fe9a260ce4db488a86a6558df7f
Author: Dominik Riemer <ri...@fzi.de>
AuthorDate: Fri Jun 12 10:21:57 2020 +0200

    [STREAMPIPES-145] Migrate pipeline start dialog
---
 .../model/pipeline/PipelineCategory.java           |   2 +
 .../model/pipeline/PipelineOperationStatus.java    |   3 +
 .../streampipes/rest/impl/PipelineCategory.java    |  12 +-
 .../rest/impl/PipelineElementCategory.java         |  19 +--
 .../rest/impl/PipelineWithUserResource.java        |  10 +-
 ui/src/app/core-model/gen/streampipes-model.ts     |  71 ++++++++++-
 .../pipeline-assembly.component.ts                 |  19 ++-
 ...mize.component.css => customize.component.scss} |  17 +--
 .../dialog/customize/customize.component.ts        |   2 +-
 .../save-pipeline/save-pipeline.component.html     |  65 ++++++++++
 .../save-pipeline/save-pipeline.component.scss}    |  11 ++
 .../save-pipeline/save-pipeline.component.ts       | 137 +++++++++++++++++++++
 ui/src/app/editor-v2/editor.module.ts              |   4 +-
 ui/src/app/editor-v2/services/editor.service.ts    |  16 ++-
 .../apis/commons.service.ts}                       |  34 +++--
 .../apis/pipeline-element.service.ts               |  13 +-
 .../app/platform-services/apis/pipeline.service.ts |  89 +++++++++++++
 ...constants.ts => platform-services.constants.ts} |   4 +
 ui/src/app/platform-services/platform.module.ts    |   6 +-
 ui/src/scss/sp/dialog.scss                         |   2 +
 .../sp/sp-dialog.scss}                             |  18 ++-
 21 files changed, 478 insertions(+), 76 deletions(-)

diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineCategory.java b/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineCategory.java
index b5599bd..364dfb8 100644
--- a/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineCategory.java
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineCategory.java
@@ -19,7 +19,9 @@
 package org.apache.streampipes.model.pipeline;
 
 import com.google.gson.annotations.SerializedName;
+import org.apache.streampipes.model.shared.annotation.TsModel;
 
+@TsModel
 public class PipelineCategory {
 
 	private String categoryName;
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineOperationStatus.java b/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineOperationStatus.java
index 4f4a1d0..575bfaf 100644
--- a/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineOperationStatus.java
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/pipeline/PipelineOperationStatus.java
@@ -18,9 +18,12 @@
 
 package org.apache.streampipes.model.pipeline;
 
+import org.apache.streampipes.model.shared.annotation.TsModel;
+
 import java.util.ArrayList;
 import java.util.List;
 
+@TsModel
 public class PipelineOperationStatus {
 
 	private String pipelineId;
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCategory.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCategory.java
index 66070e2..5ac049e 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCategory.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCategory.java
@@ -19,14 +19,10 @@
 package org.apache.streampipes.rest.impl;
 
 import org.apache.streampipes.model.message.Notifications;
+import org.apache.streampipes.rest.shared.annotation.JacksonSerialized;
 import org.apache.streampipes.storage.api.IPipelineCategoryStorage;
 
-import javax.ws.rs.DELETE;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
+import javax.ws.rs.*;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
@@ -35,6 +31,7 @@ public class PipelineCategory extends AbstractRestInterface {
 
 	@GET
 	@Produces(MediaType.APPLICATION_JSON)
+	@JacksonSerialized
 	public Response getCategories(@PathParam("username") String username) {
 		return ok(getPipelineCategoryStorage()
 				.getPipelineCategories());
@@ -42,6 +39,8 @@ public class PipelineCategory extends AbstractRestInterface {
 	
 	@POST
 	@Produces(MediaType.APPLICATION_JSON)
+	@Consumes(MediaType.APPLICATION_JSON)
+	@JacksonSerialized
 	public Response addCategory(@PathParam("username") String username, org.apache.streampipes.model.pipeline.PipelineCategory pipelineCategory) {
 		boolean success = getPipelineCategoryStorage()
 				.addPipelineCategory(pipelineCategory);
@@ -52,6 +51,7 @@ public class PipelineCategory extends AbstractRestInterface {
 	@DELETE
 	@Path("/{categoryId}")
 	@Produces(MediaType.APPLICATION_JSON)
+	@JacksonSerialized
 	public Response removeCategory(@PathParam("username") String username, @PathParam("categoryId") String categoryId) {
 		boolean success = getPipelineCategoryStorage()
 				.deletePipelineCategory(categoryId);
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineElementCategory.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineElementCategory.java
index fb2a783..06aee1d 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineElementCategory.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineElementCategory.java
@@ -24,22 +24,24 @@ import org.apache.streampipes.model.DataSinkType;
 import org.apache.streampipes.model.client.Category;
 import org.apache.streampipes.model.graph.DataSourceDescription;
 import org.apache.streampipes.rest.api.IPipelineElementCategory;
+import org.apache.streampipes.rest.shared.annotation.JacksonSerialized;
 import org.apache.streampipes.storage.management.StorageManager;
 
-import java.util.List;
-import java.util.stream.Collectors;
-
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import java.util.List;
+import java.util.stream.Collectors;
 
 @Path("/v2/categories")
 public class PipelineElementCategory extends AbstractRestInterface implements IPipelineElementCategory {
 
 	@GET
 	@Path("/ep")
-	@Produces("application/json")
+	@Produces(MediaType.APPLICATION_JSON)
+	@JacksonSerialized
 	@Override
 	public Response getEps() {
 		return ok(makeCategories(StorageManager.INSTANCE.getPipelineElementStorage().getAllDataSources()));
@@ -47,7 +49,8 @@ public class PipelineElementCategory extends AbstractRestInterface implements IP
 
 	@GET
 	@Path("/epa")
-	@Produces("application/json")
+	@Produces(MediaType.APPLICATION_JSON)
+	@JacksonSerialized
 	@Override
 	public Response getEpaCategories() {
 		return ok(DataProcessorType.values());
@@ -55,7 +58,8 @@ public class PipelineElementCategory extends AbstractRestInterface implements IP
 
 	@GET
 	@Path("/adapter")
-	@Produces("application/json")
+	@Produces(MediaType.APPLICATION_JSON)
+	@JacksonSerialized
 	@Override
 	public Response getAdapterCategories() {
 		return ok(AdapterType.values());
@@ -63,7 +67,8 @@ public class PipelineElementCategory extends AbstractRestInterface implements IP
 
 	@GET
 	@Path("/ec")
-	@Produces("application/json")
+	@Produces(MediaType.APPLICATION_JSON)
+	@JacksonSerialized
 	@Override
 	public Response getEcCategories() {
 		return ok(DataSinkType.values());
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineWithUserResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineWithUserResource.java
index 5f7399a..4854c69 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineWithUserResource.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineWithUserResource.java
@@ -64,7 +64,7 @@ public class PipelineWithUserResource extends AbstractRestInterface implements I
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/own")
-    @GsonWithIds
+    @JacksonSerialized
     @Override
     public Response getOwn(@PathParam("username") String username) {
         return ok(getUserService()
@@ -74,7 +74,7 @@ public class PipelineWithUserResource extends AbstractRestInterface implements I
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/system")
-    @GsonWithIds
+    @JacksonSerialized
     @Override
     public Response getSystemPipelines() {
         return ok(getPipelineStorage().getSystemPipelines());
@@ -127,7 +127,7 @@ public class PipelineWithUserResource extends AbstractRestInterface implements I
     @Path("/{pipelineId}/start")
     @GET
     @Produces(MediaType.APPLICATION_JSON)
-    @GsonWithIds
+    @JacksonSerialized
     public Response start(@PathParam("username") String username, @PathParam("pipelineId") String pipelineId) {
         try {
             Pipeline pipeline = getPipelineStorage()
@@ -143,7 +143,7 @@ public class PipelineWithUserResource extends AbstractRestInterface implements I
     @Path("/{pipelineId}/stop")
     @GET
     @Produces(MediaType.APPLICATION_JSON)
-    @GsonWithIds
+    @JacksonSerialized
     public Response stop(@PathParam("username") String username, @PathParam("pipelineId") String pipelineId) {
         logger.info("User: " + username + " stopped pipeline: " + pipelineId);
         PipelineManagement pm = new PipelineManagement();
@@ -152,7 +152,7 @@ public class PipelineWithUserResource extends AbstractRestInterface implements I
 
     @POST
     @Produces(MediaType.APPLICATION_JSON)
-    @GsonWithIds
+    @JacksonSerialized
     public Response addPipeline(@PathParam("username") String username, Pipeline pipeline) {
         String pipelineId = UUID.randomUUID().toString();
         pipeline.setPipelineId(pipelineId);
diff --git a/ui/src/app/core-model/gen/streampipes-model.ts b/ui/src/app/core-model/gen/streampipes-model.ts
index 252605e..4d383f7 100644
--- a/ui/src/app/core-model/gen/streampipes-model.ts
+++ b/ui/src/app/core-model/gen/streampipes-model.ts
@@ -1,7 +1,7 @@
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 2.23.603 on 2020-06-10 11:14:31.
+// Generated using typescript-generator version 2.23.603 on 2020-06-11 23:05:01.
 
 export class AbstractStreamPipesEntity {
     "@class": "org.apache.streampipes.model.base.NamedStreamPipesEntity" | "org.apache.streampipes.model.connect.adapter.AdapterDescription" | "org.apache.streampipes.model.connect.adapter.AdapterSetDescription" | "org.apache.streampipes.model.connect.adapter.GenericAdapterSetDescription" | "org.apache.streampipes.model.connect.adapter.SpecificAdapterSetDescription" | "org.apache.streampipes.model.connect.adapter.AdapterStreamDescription" | "org.apache.streampipes.model.connect.adapter.G [...]
@@ -133,8 +133,8 @@ export class NamedStreamPipesEntity extends AbstractStreamPipesEntity {
         instance.includedLocales = __getCopyArrayFn(__identity<string>())(data.includedLocales);
         instance.applicationLinks = __getCopyArrayFn(ApplicationLink.fromData)(data.applicationLinks);
         instance.connectedTo = __getCopyArrayFn(__identity<string>())(data.connectedTo);
-        instance.dom = data.dom;
         instance.uri = data.uri;
+        instance.dom = data.dom;
         return instance;
     }
 }
@@ -169,9 +169,9 @@ export class AdapterDescription extends NamedStreamPipesEntity {
         instance.config = __getCopyArrayFn(StaticProperty.fromDataUnion)(data.config);
         instance.rules = __getCopyArrayFn(TransformationRuleDescription.fromDataUnion)(data.rules);
         instance.category = __getCopyArrayFn(__identity<string>())(data.category);
+        instance.schemaRules = __getCopyArrayFn(__identity<any>())(data.schemaRules);
         instance.streamRules = __getCopyArrayFn(__identity<any>())(data.streamRules);
         instance.valueRules = __getCopyArrayFn(__identity<any>())(data.valueRules);
-        instance.schemaRules = __getCopyArrayFn(__identity<any>())(data.schemaRules);
         instance.couchDBId = data.couchDBId;
         instance._rev = data._rev;
         return instance;
@@ -1378,9 +1378,9 @@ export class GenericAdapterSetDescription extends AdapterSetDescription implemen
         }
         const instance = target || new GenericAdapterSetDescription();
         super.fromData(data, instance);
-        instance.eventSchema = EventSchema.fromData(data.eventSchema);
         instance.formatDescription = FormatDescription.fromData(data.formatDescription);
         instance.protocolDescription = ProtocolDescription.fromData(data.protocolDescription);
+        instance.eventSchema = EventSchema.fromData(data.eventSchema);
         return instance;
     }
 }
@@ -1397,9 +1397,9 @@ export class GenericAdapterStreamDescription extends AdapterStreamDescription im
         }
         const instance = target || new GenericAdapterStreamDescription();
         super.fromData(data, instance);
-        instance.eventSchema = EventSchema.fromData(data.eventSchema);
         instance.formatDescription = FormatDescription.fromData(data.formatDescription);
         instance.protocolDescription = ProtocolDescription.fromData(data.protocolDescription);
+        instance.eventSchema = EventSchema.fromData(data.eventSchema);
         return instance;
     }
 }
@@ -1791,6 +1791,25 @@ export class Pipeline extends ElementComposition {
     }
 }
 
+export class PipelineCategory {
+    categoryDescription: string;
+    categoryId: string;
+    categoryName: string;
+    rev: string;
+
+    static fromData(data: PipelineCategory, target?: PipelineCategory): PipelineCategory {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new PipelineCategory();
+        instance.categoryName = data.categoryName;
+        instance.categoryDescription = data.categoryDescription;
+        instance.categoryId = data.categoryId;
+        instance.rev = data.rev;
+        return instance;
+    }
+}
+
 export class PipelineElementRecommendation {
     count: number;
     description: string;
@@ -1829,6 +1848,25 @@ export class PipelineElementRecommendationMessage {
     }
 }
 
+export class PipelineElementStatus {
+    elementId: string;
+    elementName: string;
+    optionalMessage: string;
+    success: boolean;
+
+    static fromData(data: PipelineElementStatus, target?: PipelineElementStatus): PipelineElementStatus {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new PipelineElementStatus();
+        instance.elementId = data.elementId;
+        instance.elementName = data.elementName;
+        instance.optionalMessage = data.optionalMessage;
+        instance.success = data.success;
+        return instance;
+    }
+}
+
 export class PipelineModification {
     domId: string;
     elementId: string;
@@ -1864,6 +1902,27 @@ export class PipelineModificationMessage extends Message {
     }
 }
 
+export class PipelineOperationStatus {
+    elementStatus: PipelineElementStatus[];
+    pipelineId: string;
+    pipelineName: string;
+    success: boolean;
+    title: string;
+
+    static fromData(data: PipelineOperationStatus, target?: PipelineOperationStatus): PipelineOperationStatus {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new PipelineOperationStatus();
+        instance.pipelineId = data.pipelineId;
+        instance.pipelineName = data.pipelineName;
+        instance.title = data.title;
+        instance.success = data.success;
+        instance.elementStatus = __getCopyArrayFn(PipelineElementStatus.fromData)(data.elementStatus);
+        return instance;
+    }
+}
+
 export class Precision extends EventPropertyQualityDefinition {
     "@class": "org.apache.streampipes.model.quality.Precision";
     quantityValue: number;
@@ -2196,8 +2255,8 @@ export class SpDataSet extends SpDataStream {
         instance.supportedGrounding = EventGrounding.fromData(data.supportedGrounding);
         instance.datasetInvocationId = data.datasetInvocationId;
         instance.correspondingPipeline = data.correspondingPipeline;
-        instance.brokerHostname = data.brokerHostname;
         instance.actualTopicName = data.actualTopicName;
+        instance.brokerHostname = data.brokerHostname;
         return instance;
     }
 }
diff --git a/ui/src/app/editor-v2/components/pipeline-assembly/pipeline-assembly.component.ts b/ui/src/app/editor-v2/components/pipeline-assembly/pipeline-assembly.component.ts
index 62c10a4..ce9725e 100644
--- a/ui/src/app/editor-v2/components/pipeline-assembly/pipeline-assembly.component.ts
+++ b/ui/src/app/editor-v2/components/pipeline-assembly/pipeline-assembly.component.ts
@@ -33,6 +33,11 @@ import {
     PipelineElementHolder,
     PipelineElementUnion
 } from "../../model/editor.model";
+import {ObjectProvider} from "../../services/object-provider.service";
+import {CustomizeComponent} from "../../dialog/customize/customize.component";
+import {PanelType} from "../../../core-ui/dialog/base-dialog/base-dialog.model";
+import {SavePipelineComponent} from "../../dialog/save-pipeline/save-pipeline.component";
+import {DialogService} from "../../../core-ui/dialog/base-dialog/base-dialog.service";
 
 
 @Component({
@@ -43,7 +48,6 @@ import {
 export class PipelineAssemblyComponent implements OnInit {
 
     PipelineEditorService: any;
-    ObjectProvider: any;
     DialogBuilder: any;
     currentMouseOverElement: any;
     currentZoomLevel: any;
@@ -74,12 +78,14 @@ export class PipelineAssemblyComponent implements OnInit {
 
     constructor(private JsplumbBridge: JsplumbBridge,
                 private PipelinePositioningService: PipelinePositioningService,
+                private ObjectProvider: ObjectProvider,
                 //private EditorDialogManager: EditorDialogManager,
                 public PipelineValidationService: PipelineValidationService,
                 private RestApi: RestApi,
                 private JsplumbService: JsplumbService,
                 //private TransitionService: TransitionService,
-                private ShepherdService: ShepherdService) {
+                private ShepherdService: ShepherdService,
+                private dialogService: DialogService) {
 
         this.selectMode = true;
         this.currentZoomLevel = 1;
@@ -192,7 +198,14 @@ export class PipelineAssemblyComponent implements OnInit {
             pipeline._id = this.currentModifiedPipelineId;
         }
 
-        this.openPipelineNameModal(pipeline, (!!this.currentModifiedPipelineId));
+        const dialogRef = this.dialogService.open(SavePipelineComponent,{
+            panelType: PanelType.SLIDE_IN_PANEL,
+            title: "Save pipeline",
+            data: {
+                "pipeline": pipeline,
+                "currentModifiedPipelineId": this.currentModifiedPipelineId
+            }
+        });
     }
 
 
diff --git a/ui/src/app/editor-v2/dialog/customize/customize.component.css b/ui/src/app/editor-v2/dialog/customize/customize.component.scss
similarity index 79%
copy from ui/src/app/editor-v2/dialog/customize/customize.component.css
copy to ui/src/app/editor-v2/dialog/customize/customize.component.scss
index 6abbbdd..d043e55 100644
--- a/ui/src/app/editor-v2/dialog/customize/customize.component.css
+++ b/ui/src/app/editor-v2/dialog/customize/customize.component.scss
@@ -16,22 +16,7 @@
  *
  */
 
-.dialog-container {
-    display: flex;
-    flex-flow: column;
-    align-items: stretch;
-    flex: 1 1 100%;
-    height:100%;
-}
-
-.mat-dialog-content {
-    margin: 0px;
-    flex: 1 1 auto;
-}
-
-.mat-dialog-actions {
-    padding: 10px;
-}
+@import '../../../../scss/sp/sp-dialog.scss';
 
 .customize-section {
     display:flex;
diff --git a/ui/src/app/editor-v2/dialog/customize/customize.component.ts b/ui/src/app/editor-v2/dialog/customize/customize.component.ts
index d6b44e4..174f502 100644
--- a/ui/src/app/editor-v2/dialog/customize/customize.component.ts
+++ b/ui/src/app/editor-v2/dialog/customize/customize.component.ts
@@ -25,7 +25,7 @@ import {EventSchema} from "../../../core-model/gen/streampipes-model";
 @Component({
   selector: 'customize-pipeline-element',
   templateUrl: './customize.component.html',
-  styleUrls: ['./customize.component.css']
+  styleUrls: ['./customize.component.scss']
 })
 export class CustomizeComponent implements OnInit {
 
diff --git a/ui/src/app/editor-v2/dialog/save-pipeline/save-pipeline.component.html b/ui/src/app/editor-v2/dialog/save-pipeline/save-pipeline.component.html
new file mode 100644
index 0000000..0c1aaf4
--- /dev/null
+++ b/ui/src/app/editor-v2/dialog/save-pipeline/save-pipeline.component.html
@@ -0,0 +1,65 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~    http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  -->
+
+<div class="dialog-container">
+    <div class="mat-dialog-content padding-20">
+        <div fxFlex="100" fxLayout="column">
+
+        <form name="submitPipelineForm">
+            <div fxFlex="100" fxLayout="column">
+                <div id="overwriteCheckbox" class="checkbox" *ngIf="modificationMode">
+                    <mat-radio-group class="md-accent" [(ngModel)]="updateMode">
+                        <mat-radio-button [value]="'update'">
+                            Update pipeline <b>{{pipeline.name}}</b>
+                        </mat-radio-button>
+                        <mat-radio-button [value]="'clone'">
+                            Create new pipeline
+                        </mat-radio-button>
+                    </mat-radio-group>
+                </div>
+                <div fxFlex="100" fxLayout="column" *ngIf="!modificationMode || updateMode=='clone'">
+                    <mat-form-field fxFlex><mat-label>Pipeline Name</mat-label>
+                        <input matInput name="pipelineName" [(ngModel)]="pipeline.name" required [maxLength]="40"/>
+<!--                        <span ng-show="submitPipelineForm.pipelineName.$touched && submitPipelineForm.pipelineName.$error.required">Please provide a pipeline name.</span>-->
+<!--                        <span ng-show="submitPipelineForm.pipelineName.$error.maxlength">Please provide a shorter pipeline name.</span>-->
+                    </mat-form-field>
+                    <mat-form-field fxFlex><mat-label>Description</mat-label>
+                        <input matInput name="pipelineDescription" [(ngModel)]="pipeline.description" [maxLength]="80"/>
+<!--                        <span ng-show="submitPipelineForm.pipelineDescription.$error.maxlength">Please provide a shorter description.</span>-->
+                    </mat-form-field>
+                </div>
+                <mat-checkbox (click)="triggerTutorial()" aria-label="Start Pipeline" [(ngModel)]="startPipelineAfterStorage">
+                    Start pipeline immediately
+                </mat-checkbox>
+            </div>
+        </form>
+        </div>
+    </div>
+    <div class="mat-dialog-actions">
+        <mat-divider style="margin-bottom:10px;"></mat-divider>
+        <button mat-button mat-raised-button color="primary" (click)="savePipelineName(false)" style="margin-right:10px;">
+            Save
+        </button>
+        <button mat-button mat-raised-button color="primary" (click)="savePipelineName(true)" style="margin-right:10px;">
+            Save and go to pipeline view
+        </button>
+        <button mat-button mat-raised-button class="mat-basic" (click)="hide()">
+            Cancel
+        </button>
+    </div>
+</div>
\ No newline at end of file
diff --git a/ui/src/app/platform-services/contants/pipeline-element-constants.ts b/ui/src/app/editor-v2/dialog/save-pipeline/save-pipeline.component.scss
similarity index 83%
copy from ui/src/app/platform-services/contants/pipeline-element-constants.ts
copy to ui/src/app/editor-v2/dialog/save-pipeline/save-pipeline.component.scss
index 58ba04b..b2f7ca6 100644
--- a/ui/src/app/platform-services/contants/pipeline-element-constants.ts
+++ b/ui/src/app/editor-v2/dialog/save-pipeline/save-pipeline.component.scss
@@ -16,3 +16,14 @@
  *
  */
 
+@import '../../../../scss/sp/sp-dialog.scss';
+
+.customize-section {
+  display:flex;
+  flex: 1 1 auto;
+  padding: 20px;
+}
+
+.padding-20 {
+  padding: 20px;
+}
\ No newline at end of file
diff --git a/ui/src/app/editor-v2/dialog/save-pipeline/save-pipeline.component.ts b/ui/src/app/editor-v2/dialog/save-pipeline/save-pipeline.component.ts
new file mode 100644
index 0000000..836ada2
--- /dev/null
+++ b/ui/src/app/editor-v2/dialog/save-pipeline/save-pipeline.component.ts
@@ -0,0 +1,137 @@
+/*
+ * 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, Inject, Input, OnInit} from "@angular/core";
+import {DialogRef} from "../../../core-ui/dialog/base-dialog/dialog-ref";
+import {Message, Pipeline} from "../../../core-model/gen/streampipes-model";
+import {ObjectProvider} from "../../services/object-provider.service";
+import {EditorService} from "../../services/editor.service";
+import {PipelineService} from "../../../platform-services/apis/pipeline.service";
+import {ShepherdService} from "../../../services/tour/shepherd.service";
+
+@Component({
+  selector: 'save-pipeline',
+  templateUrl: './save-pipeline.component.html',
+  styleUrls: ['./save-pipeline.component.scss']
+})
+export class SavePipelineComponent implements OnInit {
+
+  pipelineCategories: any;
+  startPipelineAfterStorage: any;
+  updateMode: any;
+  submitPipelineForm: any;
+  TransitionService: any;
+
+  @Input()
+  pipeline: Pipeline;
+  @Input()
+  modificationMode: string;
+
+  constructor(private editorService: EditorService,
+              private dialogRef: DialogRef<SavePipelineComponent>,
+              private objectProvider: ObjectProvider,
+              private pipelineService: PipelineService,
+              //TransitionService,
+              @Inject("$state") private $state: any,
+              private ShepherdService: ShepherdService) {
+    this.pipelineCategories = [];
+    this.updateMode = "update";
+    //this.TransitionService = TransitionService;
+    //this.ShepherdService = ShepherdService;
+  }
+
+  ngOnInit() {
+    this.getPipelineCategories();
+    if (this.ShepherdService.isTourActive()) {
+      this.ShepherdService.trigger("enter-pipeline-name");
+    }
+
+  }
+
+  triggerTutorial() {
+    if (this.ShepherdService.isTourActive()) {
+      this.ShepherdService.trigger("save-pipeline-dialog");
+    }
+  }
+
+  displayErrors(data) {
+    for (var i = 0, notification; notification = data.notifications[i]; i++) {
+      //this.showToast("error", notification.title, notification.description);
+    }
+  }
+
+  displaySuccess(data) {
+    if (data.notifications.length > 0) {
+      //this.showToast("success", data.notifications[0].title, data.notifications[0].description);
+    }
+  }
+
+  getPipelineCategories() {
+    this.pipelineService.getPipelineCategories().subscribe(pipelineCategories => {
+          this.pipelineCategories = pipelineCategories;
+        });
+  };
+
+
+  savePipelineName(switchTab) {
+    if (this.pipeline.name == "") {
+      //this.showToast("error", "Please enter a name for your pipeline");
+      return false;
+    }
+
+    let storageRequest;
+
+    if (this.modificationMode && this.updateMode === 'update') {
+      storageRequest = this.pipelineService.updatePipeline(this.pipeline);
+    } else {
+      storageRequest = this.pipelineService.storePipeline(this.pipeline);
+    }
+
+    storageRequest
+        .subscribe(statusMessage => {
+          if (statusMessage.success) {
+            this.afterStorage(statusMessage, switchTab);
+          } else {
+            this.displayErrors(statusMessage);
+          }
+        }, data => {
+          //this.showToast("error", "Connection Error", "Could not fulfill request");
+        });
+  };
+
+  afterStorage(data: Message, switchTab) {
+    this.displaySuccess(data);
+    this.hide();
+    //this.TransitionService.makePipelineAssemblyEmpty(true);
+    this.editorService.removePipelineFromCache();
+    if (this.ShepherdService.isTourActive()) {
+      this.ShepherdService.hideCurrentStep();
+    }
+    if (switchTab && !this.startPipelineAfterStorage) {
+      this.$state.go("streampipes.pipelines");
+    }
+    if (this.startPipelineAfterStorage) {
+      this.$state.go("streampipes.pipelines", {pipeline: data.notifications[1].description});
+    }
+  }
+
+  hide() {
+    this.dialogRef.close();
+    //this.$mdDialog.hide();
+  };
+}
\ No newline at end of file
diff --git a/ui/src/app/editor-v2/editor.module.ts b/ui/src/app/editor-v2/editor.module.ts
index 08f173c..19f5141 100644
--- a/ui/src/app/editor-v2/editor.module.ts
+++ b/ui/src/app/editor-v2/editor.module.ts
@@ -48,6 +48,7 @@ import {OverlayModule} from "@angular/cdk/overlay";
 import {CustomizeComponent} from "./dialog/customize/customize.component";
 import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
 import {CoreUiModule} from "../core-ui/core-ui.module";
+import {SavePipelineComponent} from "./dialog/save-pipeline/save-pipeline.component";
 
 @NgModule({
     imports: [
@@ -70,7 +71,8 @@ import {CoreUiModule} from "../core-ui/core-ui.module";
         PipelineElementIconStandComponent,
         PipelineElementComponent,
         PipelineElementOptionsComponent,
-        PipelineComponent
+        PipelineComponent,
+        SavePipelineComponent
     ],
     providers: [
         EditorService,
diff --git a/ui/src/app/editor-v2/services/editor.service.ts b/ui/src/app/editor-v2/services/editor.service.ts
index 309c386..6f34eb1 100644
--- a/ui/src/app/editor-v2/services/editor.service.ts
+++ b/ui/src/app/editor-v2/services/editor.service.ts
@@ -25,11 +25,13 @@ import {
     PipelineModificationMessage
 } from "../../core-model/gen/streampipes-model";
 import {Observable} from "rxjs";
+import {PlatformServicesCommons} from "../../platform-services/apis/commons.service";
 
 @Injectable()
 export class EditorService {
 
     constructor(private http: HttpClient,
+                private platformServicesCommons: PlatformServicesCommons,
                 private authStatusService: AuthStatusService) {
     }
 
@@ -45,12 +47,20 @@ export class EditorService {
             });
     }
 
-    private get baseUrl() {
-        return '/streampipes-backend';
+    getCachedPipeline() {
+        return this.http.get(this.platformServicesCommons.authUserBasePath() + "/pipeline-cache");
+    }
+
+    updateCachedPipeline(rawPipelineModel: any) {
+        return this.http.post(this.platformServicesCommons.authUserBasePath() + "/pipeline-cache", rawPipelineModel);
+    }
+
+    removePipelineFromCache() {
+        return this.http.delete(this.platformServicesCommons.authUserBasePath() + "/pipeline-cache");
     }
 
     private get pipelinesResourceUrl() {
-        return this.baseUrl + '/api/v2/users/' + this.authStatusService.email + '/pipelines'
+        return this.platformServicesCommons.authUserBasePath() + '/pipelines'
     }
 
 
diff --git a/ui/src/app/editor-v2/dialog/customize/customize.component.css b/ui/src/app/platform-services/apis/commons.service.ts
similarity index 66%
rename from ui/src/app/editor-v2/dialog/customize/customize.component.css
rename to ui/src/app/platform-services/apis/commons.service.ts
index 6abbbdd..2c38292 100644
--- a/ui/src/app/editor-v2/dialog/customize/customize.component.css
+++ b/ui/src/app/platform-services/apis/commons.service.ts
@@ -16,25 +16,23 @@
  *
  */
 
-.dialog-container {
-    display: flex;
-    flex-flow: column;
-    align-items: stretch;
-    flex: 1 1 100%;
-    height:100%;
-}
+import {AuthStatusService} from "../../services/auth-status.service";
+import {Injectable} from "@angular/core";
 
-.mat-dialog-content {
-    margin: 0px;
-    flex: 1 1 auto;
-}
+@Injectable()
+export class PlatformServicesCommons {
+
+  constructor(private authStatusService: AuthStatusService) {
+
+  }
+
+  get basePath(): string {
+    return '/streampipes-backend';
+  }
+
+  authUserBasePath() {
+    return this.basePath + '/api/v2/users/' + this.authStatusService.email;
+  }
 
-.mat-dialog-actions {
-    padding: 10px;
 }
 
-.customize-section {
-    display:flex;
-    flex: 1 1 auto;
-    padding: 20px;
-}
\ No newline at end of file
diff --git a/ui/src/app/platform-services/apis/pipeline-element.service.ts b/ui/src/app/platform-services/apis/pipeline-element.service.ts
index 31c1b79..0463376 100644
--- a/ui/src/app/platform-services/apis/pipeline-element.service.ts
+++ b/ui/src/app/platform-services/apis/pipeline-element.service.ts
@@ -25,12 +25,13 @@ import {
   DataSinkInvocation,
   DataSourceDescription
 } from "../../core-model/gen/streampipes-model";
+import {PlatformServicesCommons} from "./commons.service";
 
 @Injectable()
 export class PipelineElementService {
 
   constructor(private http: HttpClient,
-              private authStatusService: AuthStatusService) {
+              private platformServicesCommons: PlatformServicesCommons) {
   }
 
   getDataProcessors(): Observable<Array<DataProcessorInvocation>> {
@@ -51,19 +52,15 @@ export class PipelineElementService {
     })
   }
 
-  private get baseUrl(): string {
-    return '/streampipes-backend';
-  }
-
   private get dataProcessorsUrl(): string {
-    return this.baseUrl + '/api/v2/users/' + this.authStatusService.email + '/sepas'
+    return this.platformServicesCommons.authUserBasePath() + '/sepas'
   }
 
   private get dataSourcesUrl(): string {
-    return this.baseUrl + '/api/v2/users/' + this.authStatusService.email + '/sources'
+    return this.platformServicesCommons.authUserBasePath() + '/sources'
   }
 
   private get dataSinksUrl(): string {
-    return this.baseUrl + '/api/v2/users/' + this.authStatusService.email + '/actions'
+    return this.platformServicesCommons.authUserBasePath() + '/actions'
   }
 }
\ No newline at end of file
diff --git a/ui/src/app/platform-services/apis/pipeline.service.ts b/ui/src/app/platform-services/apis/pipeline.service.ts
new file mode 100644
index 0000000..91637bc
--- /dev/null
+++ b/ui/src/app/platform-services/apis/pipeline.service.ts
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {Injectable} from "@angular/core";
+import {HttpClient} from "@angular/common/http";
+import {PlatformServicesCommons} from "./commons.service";
+import {Observable} from "rxjs";
+import {Message, Pipeline} from "../../core-model/gen/streampipes-model";
+import {map} from "rxjs/operators";
+
+@Injectable()
+export class PipelineService {
+
+  constructor(private http: HttpClient,
+              private platformServicesCommons: PlatformServicesCommons) {
+
+  }
+
+  getPipelineCategories() {
+    return this.http.get(this.platformServicesCommons.authUserBasePath() + "/pipelinecategories");
+  };
+
+  storePipelineCategory(pipelineCategory) {
+    return this.http.post(this.platformServicesCommons.authUserBasePath() + "/pipelinecategories", pipelineCategory);
+  };
+
+  deletePipelineCategory(categoryId) {
+    return this.http.delete(this.platformServicesCommons.authUserBasePath() + "/pipelinecategories/" + categoryId);
+  }
+
+  startPipeline(pipelineId) {
+    return this.http.get(this.platformServicesCommons.authUserBasePath() + "/pipelines/" + pipelineId + "/start");
+  }
+
+  stopPipeline(pipelineId) {
+    return this.http.get(this.platformServicesCommons.authUserBasePath() + "/pipelines/" + pipelineId + "/stop");
+  }
+
+  getPipelineById(pipelineId) {
+    return this.http.get(this.platformServicesCommons.authUserBasePath() + "/pipelines/" + pipelineId);
+  }
+
+  getPipelineStatusById(pipelineId) {
+    return this.http.get(this.platformServicesCommons.authUserBasePath() + "/pipelines/" + pipelineId + "/status");
+  }
+
+  storePipeline(pipeline): Observable<Message> {
+    return this.http.post(this.platformServicesCommons.authUserBasePath() + "/pipelines", pipeline)
+        .pipe(map(response => {
+          return Message.fromData(response as Message);
+        }));
+  }
+
+  updatePipeline(pipeline): Observable<Message> {
+    var pipelineId = pipeline._id;
+    return this.http.put(this.platformServicesCommons.authUserBasePath() + "/pipelines/" + pipelineId, pipeline)
+        .pipe(map(response => {
+          return Message.fromData(response as Message);
+        }));
+  }
+
+  getOwnPipelines(): Observable<Pipeline[]> {
+    return this.http.get(this.platformServicesCommons.authUserBasePath() + "/pipelines/own").pipe(map(response => {
+      return (response as any[]).map(p => Pipeline.fromData(p));
+    }));
+  };
+
+  getSystemPipelines(): Observable<Pipeline[]> {
+    return this.http.get(this.platformServicesCommons.authUserBasePath() + "/pipelines/system").pipe(map(response => {
+      return (response as any[]).map(p => Pipeline.fromData(p));
+    }))
+  };
+
+}
\ No newline at end of file
diff --git a/ui/src/app/platform-services/contants/pipeline-element-constants.ts b/ui/src/app/platform-services/contants/platform-services.constants.ts
similarity index 94%
copy from ui/src/app/platform-services/contants/pipeline-element-constants.ts
copy to ui/src/app/platform-services/contants/platform-services.constants.ts
index 58ba04b..1879aa6 100644
--- a/ui/src/app/platform-services/contants/pipeline-element-constants.ts
+++ b/ui/src/app/platform-services/contants/platform-services.constants.ts
@@ -16,3 +16,7 @@
  *
  */
 
+export class PlatformServicesConstants {
+
+}
+
diff --git a/ui/src/app/platform-services/platform.module.ts b/ui/src/app/platform-services/platform.module.ts
index b4f2ca9..1d235d2 100644
--- a/ui/src/app/platform-services/platform.module.ts
+++ b/ui/src/app/platform-services/platform.module.ts
@@ -20,14 +20,18 @@ import {NgModule} from '@angular/core';
 import {TsonLdSerializerService} from './tsonld-serializer.service';
 import {PipelineTemplateService} from './apis/pipeline-template.service';
 import {PipelineElementService} from "./apis/pipeline-element.service";
+import {PipelineService} from "./apis/pipeline.service";
+import {PlatformServicesCommons} from "./apis/commons.service";
 
 @NgModule({
   imports: [],
   declarations: [],
   providers: [
+    PlatformServicesCommons,
     TsonLdSerializerService,
     PipelineTemplateService,
-    PipelineElementService
+    PipelineElementService,
+    PipelineService
   ],
   entryComponents: []
 })
diff --git a/ui/src/scss/sp/dialog.scss b/ui/src/scss/sp/dialog.scss
index 380fda1..5e64bdd 100644
--- a/ui/src/scss/sp/dialog.scss
+++ b/ui/src/scss/sp/dialog.scss
@@ -16,6 +16,8 @@
  *
  */
 
+@import '../variables';
+
 .sp-no-padding-dialog .mat-dialog-container {
   box-shadow: 0 11px 15px -7px rgba(0,0,0,.2), 0 24px 38px 3px rgba(0,0,0,.14), 0 9px 46px 8px rgba(0,0,0,.12);
   display: block;
diff --git a/ui/src/app/platform-services/contants/pipeline-element-constants.ts b/ui/src/scss/sp/sp-dialog.scss
similarity index 78%
rename from ui/src/app/platform-services/contants/pipeline-element-constants.ts
rename to ui/src/scss/sp/sp-dialog.scss
index 58ba04b..6ff62bf 100644
--- a/ui/src/app/platform-services/contants/pipeline-element-constants.ts
+++ b/ui/src/scss/sp/sp-dialog.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,3 +16,19 @@
  *
  */
 
+.dialog-container {
+  display: flex;
+  flex-flow: column;
+  align-items: stretch;
+  flex: 1 1 100%;
+  height:100%;
+}
+
+.mat-dialog-content {
+  margin: 0px;
+  flex: 1 1 auto;
+}
+
+.mat-dialog-actions {
+  padding: 10px;
+}
\ No newline at end of file