You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ri...@apache.org on 2021/01/20 14:13:57 UTC

[incubator-streampipes] branch STREAMPIPES-272 updated: [STREAMPIPES-275] Add support for pipeline element templates in UI

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

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


The following commit(s) were added to refs/heads/STREAMPIPES-272 by this push:
     new 6fcb15b  [STREAMPIPES-275] Add support for pipeline element templates in UI
6fcb15b is described below

commit 6fcb15bcb9779d6d49f7df2da6165ccb2308b95b
Author: Dominik Riemer <ri...@fzi.de>
AuthorDate: Wed Jan 20 15:13:39 2021 +0100

    [STREAMPIPES-275] Add support for pipeline element templates in UI
---
 .../template/PipelineElementTemplateHandler.java   |  8 ++
 .../template/PipelineElementTemplateVisitor.java   | 45 +++++++---
 ui/src/app/core-model/gen/streampipes-model.ts     | 39 +++++++--
 ...pipeline-element-template-config.component.html | 60 ++++++++++++++
 ...ipeline-element-template-config.component.scss} | 15 ++++
 .../pipeline-element-template-config.component.ts  | 79 ++++++++++++++++++
 .../pipeline-element-template-generator.ts         | 45 ++++++++++
 .../dialog/customize/customize.component.html      | 76 ++++++++++++-----
 .../dialog/customize/customize.component.scss      | 11 +++
 .../editor/dialog/customize/customize.component.ts | 96 +++++++++++++++++++---
 ui/src/app/editor/editor.module.ts                 |  4 +-
 .../apis/pipeline-element-template.service.ts      | 69 ++++++++++++++++
 ui/src/app/platform-services/platform.module.ts    |  2 +
 ui/src/scss/sp/sp-dialog.scss                      |  2 +-
 14 files changed, 497 insertions(+), 54 deletions(-)

diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineElementTemplateHandler.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineElementTemplateHandler.java
index 4ae13de..1f74717 100644
--- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineElementTemplateHandler.java
+++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineElementTemplateHandler.java
@@ -25,17 +25,25 @@ public abstract class PipelineElementTemplateHandler<T extends InvocableStreamPi
   protected T pipelineElement;
   protected PipelineElementTemplate template;
 
+  private boolean overwriteNameAndDescription;
+
   public PipelineElementTemplateHandler(PipelineElementTemplate template,
                                         T pipelineElement,
                                         boolean overwriteNameAndDescription) {
     this.template = template;
     this.pipelineElement = pipelineElement;
+    this.overwriteNameAndDescription = overwriteNameAndDescription;
   }
 
   public T applyTemplateOnPipelineElement() {
       PipelineElementTemplateVisitor visitor = new PipelineElementTemplateVisitor(template);
       pipelineElement.getStaticProperties().forEach(config -> config.accept(visitor));
 
+      if (overwriteNameAndDescription) {
+        pipelineElement.setName(template.getTemplateName());
+        pipelineElement.setDescription(template.getTemplateDescription());
+      }
+
       return pipelineElement;
   }
 }
diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineElementTemplateVisitor.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineElementTemplateVisitor.java
index fc55d28..11c9583 100644
--- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineElementTemplateVisitor.java
+++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/template/PipelineElementTemplateVisitor.java
@@ -21,6 +21,7 @@ import org.apache.streampipes.model.staticproperty.*;
 import org.apache.streampipes.model.template.PipelineElementTemplate;
 import org.apache.streampipes.model.template.PipelineElementTemplateConfig;
 
+import java.util.List;
 import java.util.Map;
 
 public class PipelineElementTemplateVisitor implements StaticPropertyVisitor {
@@ -33,7 +34,12 @@ public class PipelineElementTemplateVisitor implements StaticPropertyVisitor {
 
   @Override
   public void visit(AnyStaticProperty property) {
-
+    if (hasKey(property)) {
+      List<String> value = (List<String>) getValue(property);
+      property.getOptions().forEach(option -> {
+        option.setSelected(value.stream().anyMatch(v -> v.equals(option.getName())));
+      });
+    }
   }
 
   @Override
@@ -48,7 +54,9 @@ public class PipelineElementTemplateVisitor implements StaticPropertyVisitor {
 
   @Override
   public void visit(ColorPickerStaticProperty colorPickerStaticProperty) {
-
+    if (hasKey(colorPickerStaticProperty)) {
+      colorPickerStaticProperty.setSelectedColor(getStringValue(colorPickerStaticProperty));
+    }
   }
 
   @Override
@@ -58,34 +66,41 @@ public class PipelineElementTemplateVisitor implements StaticPropertyVisitor {
 
   @Override
   public void visit(FileStaticProperty fileStaticProperty) {
-
+    if (hasKey(fileStaticProperty)) {
+      fileStaticProperty.setLocationPath(getStringValue(fileStaticProperty));
+    }
   }
 
   @Override
   public void visit(FreeTextStaticProperty freeTextStaticProperty) {
-    if (hasKey(freeTextStaticProperty.getInternalName())) {
-      freeTextStaticProperty.setValue(String.valueOf(getValue(freeTextStaticProperty.getInternalName())));
+    if (hasKey(freeTextStaticProperty)) {
+      freeTextStaticProperty.setValue(getStringValue(freeTextStaticProperty));
     }
   }
 
   @Override
   public void visit(MappingPropertyNary mappingPropertyNary) {
-
+    // Do nothing, not supported by pipeline element templates
   }
 
   @Override
   public void visit(MappingPropertyUnary mappingPropertyUnary) {
-
+    // Do nothing, not supported by pipeline element templates
   }
 
   @Override
   public void visit(MatchingStaticProperty matchingStaticProperty) {
-
+    // Do nothing, not supported by pipeline element templates
   }
 
   @Override
   public void visit(OneOfStaticProperty oneOfStaticProperty) {
-
+    if (hasKey(oneOfStaticProperty)) {
+      String value = getStringValue(oneOfStaticProperty);
+      oneOfStaticProperty.getOptions().forEach(option -> {
+        option.setSelected(option.getName().equals(value));
+      });
+    }
   }
 
   @Override
@@ -113,11 +128,15 @@ public class PipelineElementTemplateVisitor implements StaticPropertyVisitor {
 
   }
 
-  private Object getValue(String internalName) {
-    return configs.get(internalName).getValue();
+  private String getStringValue(StaticProperty sp) {
+    return String.valueOf(getValue(sp));
+  }
+
+  private Object getValue(StaticProperty sp) {
+    return configs.get(sp.getInternalName()).getValue();
   }
 
-  private boolean hasKey(String internalName) {
-    return configs.containsKey(internalName);
+  private boolean hasKey(StaticProperty sp) {
+    return configs.containsKey(sp.getInternalName());
   }
 }
diff --git a/ui/src/app/core-model/gen/streampipes-model.ts b/ui/src/app/core-model/gen/streampipes-model.ts
index 716c1d7..835b465 100644
--- a/ui/src/app/core-model/gen/streampipes-model.ts
+++ b/ui/src/app/core-model/gen/streampipes-model.ts
@@ -19,7 +19,7 @@
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 2.27.744 on 2021-01-02 22:18:05.
+// Generated using typescript-generator version 2.27.744 on 2021-01-19 20:45:10.
 
 export class AbstractStreamPipesEntity {
     "@class": "org.apache.streampipes.model.base.AbstractStreamPipesEntity" | "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.AdapterStre [...]
@@ -151,8 +151,8 @@ export class NamedStreamPipesEntity extends AbstractStreamPipesEntity {
         instance.applicationLinks = __getCopyArrayFn(ApplicationLink.fromData)(data.applicationLinks);
         instance.internallyManaged = data.internallyManaged;
         instance.connectedTo = __getCopyArrayFn(__identity<string>())(data.connectedTo);
-        instance.dom = data.dom;
         instance.uri = data.uri;
+        instance.dom = data.dom;
         return instance;
     }
 }
@@ -187,9 +187,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.valueRules = __getCopyArrayFn(__identity<any>())(data.valueRules);
         instance.streamRules = __getCopyArrayFn(__identity<any>())(data.streamRules);
-        instance.schemaRules = __getCopyArrayFn(__identity<any>())(data.schemaRules);
         instance.couchDBId = data.couchDBId;
         instance._rev = data._rev;
         return instance;
@@ -1601,9 +1601,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.formatDescription = FormatDescription.fromData(data.formatDescription);
+        instance.eventSchema = EventSchema.fromData(data.eventSchema);
         return instance;
     }
 }
@@ -1620,9 +1620,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.formatDescription = FormatDescription.fromData(data.formatDescription);
+        instance.eventSchema = EventSchema.fromData(data.eventSchema);
         return instance;
     }
 }
@@ -2118,8 +2118,10 @@ export class PipelineElementStatus {
 }
 
 export class PipelineElementTemplate {
+    _id: string;
+    _rev: string;
     basePipelineElementAppId: string;
-    templateConfigs: { [index: string]: any };
+    templateConfigs: { [index: string]: PipelineElementTemplateConfig };
     templateDescription: string;
     templateName: string;
 
@@ -2131,7 +2133,26 @@ export class PipelineElementTemplate {
         instance.templateName = data.templateName;
         instance.templateDescription = data.templateDescription;
         instance.basePipelineElementAppId = data.basePipelineElementAppId;
-        instance.templateConfigs = __getCopyObjectFn(__identity<any>())(data.templateConfigs);
+        instance.templateConfigs = __getCopyObjectFn(PipelineElementTemplateConfig.fromData)(data.templateConfigs);
+        instance._id = data._id;
+        instance._rev = data._rev;
+        return instance;
+    }
+}
+
+export class PipelineElementTemplateConfig {
+    displayed: boolean;
+    editable: boolean;
+    value: any;
+
+    static fromData(data: PipelineElementTemplateConfig, target?: PipelineElementTemplateConfig): PipelineElementTemplateConfig {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new PipelineElementTemplateConfig();
+        instance.editable = data.editable;
+        instance.displayed = data.displayed;
+        instance.value = data.value;
         return instance;
     }
 }
diff --git a/ui/src/app/editor/components/pipeline-element-template-config/pipeline-element-template-config.component.html b/ui/src/app/editor/components/pipeline-element-template-config/pipeline-element-template-config.component.html
new file mode 100644
index 0000000..ea93938
--- /dev/null
+++ b/ui/src/app/editor/components/pipeline-element-template-config/pipeline-element-template-config.component.html
@@ -0,0 +1,60 @@
+<!--
+  ~ 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="p-15">
+    <div fxFlex="100" fxLayout="column">
+        <h4>Basics</h4>
+        <mat-form-field fxFlex>
+            <mat-label>Template name</mat-label>
+            <input [(ngModel)]="template.templateName" matInput name="templateName" class="sp" required/>
+        </mat-form-field>
+        <mat-form-field fxFlex>
+            <mat-label>Template description</mat-label>
+            <input [(ngModel)]="template.templateDescription" matInput name="templateDescription" class="sp"
+                   required/>
+        </mat-form-field>
+        <mat-checkbox class="sp" color="primary">Make available as pipeline element</mat-checkbox>
+        <mat-divider class="divider"></mat-divider>
+        <h4>Configuration</h4>
+        <div fxLayout="column" *ngFor="let config of cachedPipelineElement.staticProperties"
+             class="static-property-panel static-property-panel-border">
+            <div fxLayout="row">
+                <div fxFlex="50">
+                    {{config.label}}
+                </div>
+                <div fxFlex="50">
+                    <div fxLayout="column">
+                        <mat-checkbox [checked]="templateConfigs.has(config.internalName)"
+                                      (change)="handleSelection(config)" name="store" class="sp" color="primary">Store
+                            as template
+                        </mat-checkbox>
+                        <mat-checkbox *ngIf="templateConfigs.has(config.internalName)"
+                                      (click)="toggleViewPermission(config)" name="displayed"
+                                      class="sp" color="primary">Users can view
+                        </mat-checkbox>
+                        <mat-checkbox *ngIf="templateConfigs.has(config.internalName)"
+                                      (click)="toggleEditPermission(config)" name="editable"
+                                      class="sp" color="primary">Users can modify
+                        </mat-checkbox>
+                    </div>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>
diff --git a/ui/src/app/editor/dialog/customize/customize.component.scss b/ui/src/app/editor/components/pipeline-element-template-config/pipeline-element-template-config.component.scss
similarity index 79%
copy from ui/src/app/editor/dialog/customize/customize.component.scss
copy to ui/src/app/editor/components/pipeline-element-template-config/pipeline-element-template-config.component.scss
index fddade7..2c1a70e 100644
--- a/ui/src/app/editor/dialog/customize/customize.component.scss
+++ b/ui/src/app/editor/components/pipeline-element-template-config/pipeline-element-template-config.component.scss
@@ -17,3 +17,18 @@
  */
 
 @import '../../../../scss/sp/sp-dialog.scss';
+
+.divider {
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+
+.static-property-panel {
+  padding-left: 10px;
+  margin-bottom: 10px;
+  margin-top: 10px;
+}
+
+.static-property-panel-border {
+  border-left:5px solid gray;
+}
diff --git a/ui/src/app/editor/components/pipeline-element-template-config/pipeline-element-template-config.component.ts b/ui/src/app/editor/components/pipeline-element-template-config/pipeline-element-template-config.component.ts
new file mode 100644
index 0000000..c402757
--- /dev/null
+++ b/ui/src/app/editor/components/pipeline-element-template-config/pipeline-element-template-config.component.ts
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {Component, Input, OnInit} from "@angular/core";
+import {InvocablePipelineElementUnion} from "../../model/editor.model";
+import {
+  PipelineElementTemplate,
+  StaticPropertyUnion
+} from "../../../core-model/gen/streampipes-model";
+import {PipelineElementTemplateGenerator} from "./pipeline-element-template-generator";
+
+@Component({
+  selector: 'pipeline-element-template-config',
+  templateUrl: './pipeline-element-template-config.component.html',
+  styleUrls: ['./pipeline-element-template-config.component.scss']
+})
+export class PipelineElementTemplateConfigComponent implements OnInit {
+
+  @Input()
+  cachedPipelineElement: InvocablePipelineElementUnion;
+
+  @Input()
+  template: PipelineElementTemplate;
+
+  @Input()
+  templateConfigs: Map<string, any>;
+
+
+  ngOnInit(): void {
+    this.template.basePipelineElementAppId = this.cachedPipelineElement.appId;
+    this.cachedPipelineElement.staticProperties.forEach(sp => {
+      this.templateConfigs.set(sp.internalName, this.makeTemplateValue(sp));
+    })
+  }
+
+  handleSelection(sp: StaticPropertyUnion) {
+    if (this.templateConfigs.has(sp.internalName)) {
+      this.templateConfigs.delete(sp.internalName);
+    } else {
+      this.templateConfigs.set(sp.internalName, this.makeTemplateValue(sp));
+    }
+  }
+
+  makeTemplateValue(sp: StaticPropertyUnion) {
+    let config: any = {};
+    config.displayed = false;
+    config.editable = false;
+    config.value = new PipelineElementTemplateGenerator(sp).toTemplateValue();
+    return config;
+  }
+
+  toggleViewPermission(sp: StaticPropertyUnion) {
+    let config: any = this.templateConfigs.get(sp.internalName);
+    config.displayed = ! config.displayed;
+    this.templateConfigs.set(sp.internalName, config);
+  }
+
+  toggleEditPermission(sp: StaticPropertyUnion) {
+    let config: any = this.templateConfigs.get(sp.internalName);
+    config.editable = ! config.editable;
+    this.templateConfigs.set(sp.internalName, config);
+  }
+
+}
diff --git a/ui/src/app/editor/components/pipeline-element-template-config/pipeline-element-template-generator.ts b/ui/src/app/editor/components/pipeline-element-template-config/pipeline-element-template-generator.ts
new file mode 100644
index 0000000..f09a834
--- /dev/null
+++ b/ui/src/app/editor/components/pipeline-element-template-config/pipeline-element-template-generator.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 {
+  AnyStaticProperty,
+  ColorPickerStaticProperty,
+  FreeTextStaticProperty, OneOfStaticProperty, SecretStaticProperty,
+  StaticPropertyUnion
+} from "../../../core-model/gen/streampipes-model";
+
+export class PipelineElementTemplateGenerator {
+
+  constructor(private sp: StaticPropertyUnion) {
+
+  }
+
+  public toTemplateValue(): any {
+    if (this.sp instanceof FreeTextStaticProperty) {
+      return this.sp.value;
+    } else if (this.sp instanceof OneOfStaticProperty) {
+      return this.sp.options.find(o => o.selected).name;
+    } else if (this.sp instanceof ColorPickerStaticProperty) {
+      return this.sp.selectedColor;
+    } else if (this.sp instanceof SecretStaticProperty) {
+      return undefined;
+    } else if (this.sp instanceof AnyStaticProperty) {
+      return this.sp.options.filter(o => o.selected).map(o => o.name);
+    }
+  }
+}
diff --git a/ui/src/app/editor/dialog/customize/customize.component.html b/ui/src/app/editor/dialog/customize/customize.component.html
index 1fa0c6d..28f82d8 100644
--- a/ui/src/app/editor/dialog/customize/customize.component.html
+++ b/ui/src/app/editor/dialog/customize/customize.component.html
@@ -19,8 +19,19 @@
 <div class="sp-dialog-container">
     <div class="sp-dialog-content">
         <div fxFlex="100" fxLayout="column">
-            <div style="border-bottom:1px solid #ccc;padding:10px;background-color:#f6f6f6">
-                <div fxFlex="100" fxLayout="row" fxLayoutAlign="end end">
+            <div style="border-bottom:1px solid #ccc;padding:10px;background-color:#f6f6f6" fxLayout="row">
+                <div fxFlex fxLayoutAlign="start center" *ngIf="availableTemplates && availableTemplates.length > 0">
+                    <mat-form-field class="form-field" floatLabel="never">
+                        <mat-label>Use template</mat-label>
+                        <mat-select (selectionChange)="loadTemplate($event)" [(value)]="selectedTemplate">
+                            <mat-option>--</mat-option>
+                            <mat-option [value]="template" *ngFor="let template of availableTemplates">
+                                {{template.templateName}}
+                            </mat-option>
+                        </mat-select>
+                    </mat-form-field>
+                </div>
+                <div fxFlex fxLayout="row" fxLayoutAlign="end center">
                     <mat-slide-toggle [(ngModel)]="showDocumentation"
                                       color="primary"
                                       [disabled]="!pipelineElement.payload.includesAssets">
@@ -34,10 +45,11 @@
             </div>
             <div fxFlex="100" fxLayout="row">
                 <div fxFlex="{{_showDocumentation ? 50: 100}}">
-                    <div fxLayout="column">
+                    <div fxLayout="column" *ngIf="!templateMode">
                         <div fxLayout="row" *ngIf="restrictedEditMode">
                             <div fxLayout="column">
-                                <div fxLayout="column" class="customize-item-main-help" style="border: 2px solid #ffc400">
+                                <div fxLayout="column" class="customize-item-main-help"
+                                     style="border: 2px solid #ffc400">
                                     <div class="customize-item-title-help" style="background: #ffc400;" fxFlex="100"
                                          fxLayout="row">
                                         You can only modify things that don't affect your pipeline structure. To fully
@@ -51,7 +63,7 @@
                             <form [formGroup]="parentForm" fxFlex="100">
                                 <app-static-property *ngFor="let config of cachedPipelineElement.staticProperties"
                                                      [staticProperty]="config"
-                                                     [pipelineElement] = "cachedPipelineElement"
+                                                     [pipelineElement]="cachedPipelineElement"
                                                      [displayRecommended]="displayRecommended"
                                                      [staticProperties]="cachedPipelineElement.staticProperties"
                                                      [eventSchemas]="eventSchemas"
@@ -62,31 +74,55 @@
                                                      (validateEmitter)="validConfiguration($event)">
                                 </app-static-property>
                                 <div *ngIf="isDataProcessor">
-                                    <output-strategy *ngFor="let outputStrategy of cachedPipelineElement.outputStrategies"
-                                                     [parentForm]="parentForm"
-                                                     [outputStrategy]="outputStrategy"
-                                                     [selectedElement]="cachedPipelineElement"
-                                                     [restrictedEditMode]="restrictedEditMode">
+                                    <output-strategy
+                                            *ngFor="let outputStrategy of cachedPipelineElement.outputStrategies"
+                                            [parentForm]="parentForm"
+                                            [outputStrategy]="outputStrategy"
+                                            [selectedElement]="cachedPipelineElement"
+                                            [restrictedEditMode]="restrictedEditMode">
                                     </output-strategy>
                                 </div>
                             </form>
                         </div>
                     </div>
+                    <div fxLayout="column" *ngIf="templateMode">
+                        <pipeline-element-template-config [cachedPipelineElement]="cachedPipelineElement"
+                        [template]="template" [templateConfigs]="templateConfigs">
+                        </pipeline-element-template-config>
+                    </div>
                 </div>
-                <div fxFlex="50" *ngIf="showDocumentation" style="padding-left:10px;border-left: 2px solid rgb(204, 204, 204);">
-                    <pipeline-element-documentation [useStyling]="false" [appId]="pipelineElement.payload.appId"></pipeline-element-documentation>
+                <div fxFlex="50" *ngIf="showDocumentation"
+                     style="padding-left:10px;border-left: 2px solid rgb(204, 204, 204);">
+                    <pipeline-element-documentation [useStyling]="false"
+                                                    [appId]="pipelineElement.payload.appId"></pipeline-element-documentation>
                 </div>
             </div>
         </div>
     </div>
     <mat-divider></mat-divider>
     <div class="sp-dialog-actions">
-        <button mat-button mat-raised-button color="primary" (click)="save()" style="margin-right:10px;"
-                [disabled]="!(formValid)">
-            Save
-        </button>
-        <button mat-button mat-raised-button class="mat-basic" (click)="close()">
-            Cancel
-        </button>
+        <div fxLayout="row" *ngIf="!templateMode">
+            <button mat-button mat-raised-button color="primary" (click)="save()" style="margin-right:10px;"
+                    [disabled]="!(formValid)">
+                <i class="material-icons">save</i><span>&nbsp;Save</span>
+            </button>
+            <button mat-button mat-raised-button class="mat-basic" (click)="close()">
+                Cancel
+            </button>
+            <div fxFlex></div>
+            <button mat-button mat-raised-button color="primary" [disabled]="!(formValid)"
+                    (click)="triggerTemplateMode()">
+                <i class="material-icons">add_circle_outline</i><span>&nbsp;Create template</span>
+            </button>
+        </div>
+        <div fxLayout="row" *ngIf="templateMode">
+            <button mat-button mat-raised-button color="primary" (click)="saveTemplate()" style="margin-right:10px;"
+                    [disabled]="!(formValid)">
+                <i class="material-icons">save</i><span>&nbsp;Save template</span>
+            </button>
+            <button mat-button mat-raised-button class="mat-basic" (click)="cancelTemplateMode()">
+                Cancel
+            </button>
+        </div>
     </div>
-</div>
\ No newline at end of file
+</div>
diff --git a/ui/src/app/editor/dialog/customize/customize.component.scss b/ui/src/app/editor/dialog/customize/customize.component.scss
index fddade7..d613b95 100644
--- a/ui/src/app/editor/dialog/customize/customize.component.scss
+++ b/ui/src/app/editor/dialog/customize/customize.component.scss
@@ -17,3 +17,14 @@
  */
 
 @import '../../../../scss/sp/sp-dialog.scss';
+
+.form-field .mat-form-field-wrapper {
+  margin-bottom: -1.25em;
+}
+
+
+.form-field .mat-form-field-infix {
+  border-top: 0;
+}
+
+
diff --git a/ui/src/app/editor/dialog/customize/customize.component.ts b/ui/src/app/editor/dialog/customize/customize.component.ts
index 71ddda1..fc9c60c 100644
--- a/ui/src/app/editor/dialog/customize/customize.component.ts
+++ b/ui/src/app/editor/dialog/customize/customize.component.ts
@@ -16,19 +16,32 @@
  *
  */
 
-import {AfterViewInit, ChangeDetectorRef, Component, Input, OnInit} from "@angular/core";
+import {
+  AfterViewInit,
+  ChangeDetectorRef,
+  Component,
+  Input,
+  OnInit,
+  ViewEncapsulation
+} from "@angular/core";
 import {InvocablePipelineElementUnion, PipelineElementConfig} from "../../model/editor.model";
 import {DialogRef} from "../../../core-ui/dialog/base-dialog/dialog-ref";
 import {JsplumbService} from "../../services/jsplumb.service";
-import {DataProcessorInvocation, EventSchema} from "../../../core-model/gen/streampipes-model";
+import {
+  DataProcessorInvocation,
+  EventSchema,
+  PipelineElementTemplate, PipelineElementTemplateConfig
+} from "../../../core-model/gen/streampipes-model";
 import {FormBuilder, FormGroup} from "@angular/forms";
 import {ShepherdService} from "../../../services/tour/shepherd.service";
 import {ConfigurationInfo} from "../../../connect/model/ConfigurationInfo";
+import {PipelineElementTemplateService} from "../../../platform-services/apis/pipeline-element-template.service";
 
 @Component({
   selector: 'customize-pipeline-element',
   templateUrl: './customize.component.html',
-  styleUrls: ['./customize.component.scss']
+  styleUrls: ['./customize.component.scss'],
+  encapsulation: ViewEncapsulation.None
 })
 export class CustomizeComponent implements OnInit, AfterViewInit {
 
@@ -64,11 +77,18 @@ export class CustomizeComponent implements OnInit, AfterViewInit {
   originalDialogWidth: string | number;
   completedStaticProperty: ConfigurationInfo;
 
+  availableTemplates: Array<PipelineElementTemplate>;
+  selectedTemplate: any = false;
+  templateMode: boolean = false;
+  template: PipelineElementTemplate;
+  templateConfigs: Map<string, any> = new Map();
+
   constructor(private dialogRef: DialogRef<CustomizeComponent>,
               private JsPlumbService: JsplumbService,
               private ShepherdService: ShepherdService,
               private fb: FormBuilder,
-              private changeDetectorRef: ChangeDetectorRef) {
+              private changeDetectorRef: ChangeDetectorRef,
+              private pipelineElementTemplateService: PipelineElementTemplateService) {
 
   }
 
@@ -81,18 +101,26 @@ export class CustomizeComponent implements OnInit, AfterViewInit {
     });
     this.formValid = this.pipelineElement.settings.completed;
 
-    this.parentForm = this.fb.group({
-    });
+    this.parentForm = this.fb.group({});
 
     this.parentForm.valueChanges.subscribe(v => {
     });
 
-    this.parentForm.statusChanges.subscribe((status)=>{
+    this.parentForm.statusChanges.subscribe((status) => {
       this.formValid = this.viewInitialized && this.parentForm.valid;
     })
     if (this.ShepherdService.isTourActive()) {
-      this.ShepherdService.trigger("customize-" +this.pipelineElement.type);
+      this.ShepherdService.trigger("customize-" + this.pipelineElement.type);
     }
+    this.loadPipelineElementTemplates();
+  }
+
+  loadPipelineElementTemplates() {
+    this.pipelineElementTemplateService
+        .getPipelineElementTemplates(this.cachedPipelineElement.appId)
+        .subscribe(templates => {
+          this.availableTemplates = templates;
+        });
   }
 
   close() {
@@ -104,7 +132,7 @@ export class CustomizeComponent implements OnInit, AfterViewInit {
     this.pipelineElement.settings.completed = true;
     this.pipelineElement.payload.configured = true;
     if (this.ShepherdService.isTourActive()) {
-      this.ShepherdService.trigger("save-" +this.pipelineElement.type);
+      this.ShepherdService.trigger("save-" + this.pipelineElement.type);
     }
     this.dialogRef.close(this.pipelineElement);
   }
@@ -136,4 +164,52 @@ export class CustomizeComponent implements OnInit, AfterViewInit {
     this.completedStaticProperty = {...configurationInfo};
   }
 
-}
\ No newline at end of file
+  triggerTemplateMode() {
+    this.template = new PipelineElementTemplate();
+    this.templateMode = true;
+  }
+
+  saveTemplate() {
+    this.template.templateConfigs = this.convert(this.templateConfigs);
+    this.pipelineElementTemplateService.storePipelineElementTemplate(this.template).subscribe(result => {
+      this.loadPipelineElementTemplates();
+      this.templateMode = false;
+    });
+  }
+
+  convert(templateConfigs: Map<string, any>): any {
+    let configs: { [index: string]: PipelineElementTemplateConfig } = {};
+    templateConfigs.forEach((value, key) => {
+      configs[key] = new PipelineElementTemplateConfig();
+      configs[key].editable = value.editable;
+      configs[key].displayed = value.displayed;
+      configs[key].value = value.value;
+    });
+    return configs;
+  }
+
+  cancelTemplateMode() {
+    this.templateMode = false;
+  }
+
+  loadTemplate(event: any) {
+    if (!event.value) {
+      this.cachedPipelineElement = this.JsPlumbService.clone(this.pipelineElement.payload) as InvocablePipelineElementUnion;
+      this.selectedTemplate = false;
+    } else {
+      this.selectedTemplate = event.value;
+      if (this.cachedPipelineElement instanceof DataProcessorInvocation) {
+        this.pipelineElementTemplateService.getConfiguredDataProcessorForTemplate(event.value._id, this.cachedPipelineElement).subscribe(pe => {
+          this.cachedPipelineElement = pe as InvocablePipelineElementUnion;
+        })
+      } else {
+        this.pipelineElementTemplateService.getConfiguredDataSinkForTemplate(event.value._id, this.cachedPipelineElement).subscribe(pe => {
+          this.cachedPipelineElement = pe as InvocablePipelineElementUnion;
+        })
+      }
+
+    }
+  }
+
+
+}
diff --git a/ui/src/app/editor/editor.module.ts b/ui/src/app/editor/editor.module.ts
index db54354..511678d 100644
--- a/ui/src/app/editor/editor.module.ts
+++ b/ui/src/app/editor/editor.module.ts
@@ -59,6 +59,7 @@ import {CustomOutputStrategyComponent} from "./components/output-strategy/custom
 import {PropertySelectionComponent} from "./components/output-strategy/property-selection/property-selection.component";
 import {UserDefinedOutputStrategyComponent} from "./components/output-strategy/user-defined-output/user-defined-output.component";
 import {ConnectModule} from "../connect/connect.module";
+import {PipelineElementTemplateConfigComponent} from "./components/pipeline-element-template-config/pipeline-element-template-config.component";
 
 @NgModule({
     imports: [
@@ -93,6 +94,7 @@ import {ConnectModule} from "../connect/connect.module";
         PipelineElementIconStandComponent,
         PipelineElementOptionsComponent,
         PipelineElementRecommendationComponent,
+        PipelineElementTemplateConfigComponent,
         PipelineComponent,
         PropertySelectionComponent,
         SavePipelineComponent,
@@ -127,4 +129,4 @@ export class EditorModule {
     constructor() {
     }
 
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/platform-services/apis/pipeline-element-template.service.ts b/ui/src/app/platform-services/apis/pipeline-element-template.service.ts
new file mode 100644
index 0000000..25f6628
--- /dev/null
+++ b/ui/src/app/platform-services/apis/pipeline-element-template.service.ts
@@ -0,0 +1,69 @@
+/*
+ * 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 {Observable} from "rxjs";
+import {
+  DataProcessorInvocation,
+  DataSinkInvocation,
+  PipelineElementTemplate
+} from "../../core-model/gen/streampipes-model";
+import {PlatformServicesCommons} from "./commons.service";
+import {map} from "rxjs/operators";
+
+@Injectable()
+export class PipelineElementTemplateService {
+
+  constructor(private http: HttpClient,
+              private platformServicesCommons: PlatformServicesCommons) {
+
+  }
+
+  getPipelineElementTemplates(appId: string): Observable<Array<PipelineElementTemplate>> {
+    return this.http
+        .get(this.platformServicesCommons.authUserBasePath()
+            + "/pipeline-element-templates?appId=" + appId)
+        .pipe(map(data => {
+          console.log(data);
+          return (data as []).map(dpi => PipelineElementTemplate.fromData(dpi));
+        }));
+  }
+
+  getConfiguredDataProcessorForTemplate(templateId: string, invocation: DataProcessorInvocation): Observable<DataProcessorInvocation> {
+    return this.http.post(this.platformServicesCommons.authUserBasePath()
+        + "/pipeline-element-templates/" + templateId + "/processor", invocation)
+        .pipe(map(response => {
+          return DataProcessorInvocation.fromData(response as DataProcessorInvocation);
+        }));
+  }
+
+  getConfiguredDataSinkForTemplate(templateId: string, invocation: DataSinkInvocation): Observable<DataSinkInvocation> {
+    return this.http.post(this.platformServicesCommons.authUserBasePath()
+        + "/pipeline-element-templates/" + templateId + "/sink", invocation)
+        .pipe(map(response => {
+          return DataSinkInvocation.fromData(response as DataSinkInvocation);
+        }));
+  }
+
+  storePipelineElementTemplate(template: PipelineElementTemplate) {
+    return this.http.post(this.platformServicesCommons.authUserBasePath() + "/pipeline-element-templates", template);
+  }
+
+
+}
diff --git a/ui/src/app/platform-services/platform.module.ts b/ui/src/app/platform-services/platform.module.ts
index 85962e5..2e3cec5 100644
--- a/ui/src/app/platform-services/platform.module.ts
+++ b/ui/src/app/platform-services/platform.module.ts
@@ -23,6 +23,7 @@ import {PlatformServicesCommons} from "./apis/commons.service";
 import {PipelineElementEndpointService} from "./apis/pipeline-element-endpoint.service";
 import {FilesService} from "./apis/files.service";
 import {MeasurementUnitsService} from "./apis/measurement-units.service";
+import {PipelineElementTemplateService} from "./apis/pipeline-element-template.service";
 
 @NgModule({
   imports: [],
@@ -32,6 +33,7 @@ import {MeasurementUnitsService} from "./apis/measurement-units.service";
     MeasurementUnitsService,
     PlatformServicesCommons,
     PipelineElementEndpointService,
+    PipelineElementTemplateService,
     //PipelineTemplateService,
     PipelineElementService,
     PipelineService
diff --git a/ui/src/scss/sp/sp-dialog.scss b/ui/src/scss/sp/sp-dialog.scss
index d0bdea3..e79a646 100644
--- a/ui/src/scss/sp/sp-dialog.scss
+++ b/ui/src/scss/sp/sp-dialog.scss
@@ -39,4 +39,4 @@
 
 .p-15 {
   padding: 15px;
-}
\ No newline at end of file
+}