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/05/19 20:26:13 UTC

[incubator-streampipes] branch dev updated: [STREAMPIPES-362] Add live preview feature to pipeline editor

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

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


The following commit(s) were added to refs/heads/dev by this push:
     new a14b76f  [STREAMPIPES-362] Add live preview feature to pipeline editor
a14b76f is described below

commit a14b76f3046204d0232bc91917706f2c51aa3eec
Author: Dominik Riemer <ri...@fzi.de>
AuthorDate: Wed May 19 22:25:57 2021 +0200

    [STREAMPIPES-362] Add live preview feature to pipeline editor
---
 .../backend/StreamPipesResourceConfig.java         |   1 +
 .../model/preview/PipelinePreviewModel.java        |  38 +++++--
 .../manager/execution/http/GraphSubmitter.java     |   4 +-
 .../matching/PipelineVerificationHandler.java      |  21 ++--
 .../manager/preview/ActivePipelinePreviews.java    |  63 +++++++++++
 .../manager/preview/PipelinePreview.java           | 125 +++++++++++++++++++++
 .../rest/impl/PipelineElementPreview.java          |  64 +++++++++++
 ui/src/app/core-model/gen/streampipes-model.ts     |  22 +++-
 .../pipeline-assembly.component.html               |  18 ++-
 .../pipeline-assembly.component.scss               |  10 ++
 .../pipeline-assembly.component.ts                 |  10 +-
 .../pipeline-element-options.component.html        |   7 +-
 .../pipeline-element-preview.component.html        |  32 ++++++
 .../pipeline-element-preview.component.scss}       |  27 +++--
 .../pipeline-element-preview.component.ts          |  68 +++++++++++
 .../components/pipeline/pipeline.component.html    |  11 +-
 .../components/pipeline/pipeline.component.ts      |  63 ++++++++---
 ui/src/app/editor/editor.component.html            |   2 +-
 ui/src/app/editor/editor.component.scss            |   4 +-
 ui/src/app/editor/editor.module.ts                 |   2 +
 ui/src/app/editor/services/editor.service.ts       |  26 ++++-
 ui/src/scss/sp/pipeline-element-options.scss       |  14 ++-
 22 files changed, 559 insertions(+), 73 deletions(-)

diff --git a/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java b/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java
index 2261149..e6d9521 100644
--- a/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java
+++ b/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java
@@ -77,6 +77,7 @@ public class StreamPipesResourceConfig extends ResourceConfig {
     register(PipelineElementFile.class);
     register(PipelineElementImportNoUser.class);
     register(PipelineElementImport.class);
+    register(PipelineElementPreview.class);
     register(PipelineElementRuntimeInfo.class);
     register(PipelineMonitoring.class);
     register(PipelineNoUserResource.class);
diff --git a/ui/src/app/editor/editor.component.scss b/streampipes-model/src/main/java/org/apache/streampipes/model/preview/PipelinePreviewModel.java
similarity index 52%
copy from ui/src/app/editor/editor.component.scss
copy to streampipes-model/src/main/java/org/apache/streampipes/model/preview/PipelinePreviewModel.java
index 8aeccf2..b63db03 100644
--- a/ui/src/app/editor/editor.component.scss
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/preview/PipelinePreviewModel.java
@@ -15,21 +15,35 @@
  * limitations under the License.
  *
  */
+package org.apache.streampipes.model.preview;
 
-@import '../../scss/variables';
+import org.apache.streampipes.model.shared.annotation.TsModel;
 
-.text-color {
-  color: $sp-color-accent;
-}
+import java.util.List;
 
-.page-container {
-  border: 0;
-}
+@TsModel
+public class PipelinePreviewModel {
 
-.border {
-  border: 1px solid #cccccc;
-}
+  private String previewId;
+
+  private List<String> supportedPipelineElementDomIds;
+
+  public PipelinePreviewModel() {
+  }
+
+  public String getPreviewId() {
+    return previewId;
+  }
+
+  public void setPreviewId(String previewId) {
+    this.previewId = previewId;
+  }
+
+  public List<String> getSupportedPipelineElementDomIds() {
+    return supportedPipelineElementDomIds;
+  }
 
-.icon-stand-margin {
-  margin-bottom: 10px;
+  public void setSupportedPipelineElementDomIds(List<String> supportedPipelineElementDomIds) {
+    this.supportedPipelineElementDomIds = supportedPipelineElementDomIds;
+  }
 }
diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/http/GraphSubmitter.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/http/GraphSubmitter.java
index 1899ee1..82919a6 100644
--- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/http/GraphSubmitter.java
+++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/http/GraphSubmitter.java
@@ -38,7 +38,9 @@ public class GraphSubmitter {
 
   private final static Logger LOG = LoggerFactory.getLogger(GraphSubmitter.class);
 
-  public GraphSubmitter(String pipelineId, String pipelineName, List<InvocableStreamPipesEntity> graphs,
+  public GraphSubmitter(String pipelineId,
+                        String pipelineName,
+                        List<InvocableStreamPipesEntity> graphs,
                         List<SpDataSet> dataSets) {
     this.graphs = graphs;
     this.pipelineId = pipelineId;
diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/PipelineVerificationHandler.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/PipelineVerificationHandler.java
index 210c8c4..c6a2bb2 100644
--- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/PipelineVerificationHandler.java
+++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/matching/PipelineVerificationHandler.java
@@ -48,8 +48,13 @@ public class PipelineVerificationHandler {
   private final InvocableStreamPipesEntity rootPipelineElement;
 
   public PipelineVerificationHandler(Pipeline pipeline) throws NoSepaInPipelineException {
+    this(pipeline, PipelineVerificationUtils.getRootNode(pipeline));
+  }
+
+  public PipelineVerificationHandler(Pipeline pipeline,
+                                     InvocableStreamPipesEntity rootNode) {
     this.pipeline = pipeline;
-    this.rootPipelineElement = PipelineVerificationUtils.getRootNode(pipeline);
+    this.rootPipelineElement = rootNode;
     this.invocationGraphs = makeInvocationGraphs();
     this.pipelineModificationMessage = new PipelineModificationMessage();
   }
@@ -116,12 +121,12 @@ public class PipelineVerificationHandler {
   private void updateStaticProperties(List<SpDataStream> inputStreams,
                                       List<StaticProperty> staticProperties) {
     staticProperties
-      .stream()
-      .filter(sp -> (sp instanceof CollectionStaticProperty
-              || sp instanceof MappingProperty
-              || sp instanceof StaticPropertyGroup
-              || sp instanceof StaticPropertyAlternatives))
-      .forEach(property -> updateStaticProperty(inputStreams, property));
+            .stream()
+            .filter(sp -> (sp instanceof CollectionStaticProperty
+                    || sp instanceof MappingProperty
+                    || sp instanceof StaticPropertyGroup
+                    || sp instanceof StaticPropertyAlternatives))
+            .forEach(property -> updateStaticProperty(inputStreams, property));
   }
 
   private void updateStaticProperty(List<SpDataStream> inputStreams, StaticProperty property) {
@@ -203,4 +208,4 @@ public class PipelineVerificationHandler {
             .allMatch(pe -> pe instanceof SpDataStream);
   }
 
-}
\ No newline at end of file
+}
diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/preview/ActivePipelinePreviews.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/preview/ActivePipelinePreviews.java
new file mode 100644
index 0000000..7d46e7c
--- /dev/null
+++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/preview/ActivePipelinePreviews.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.manager.preview;
+
+import org.apache.streampipes.model.base.NamedStreamPipesEntity;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public enum ActivePipelinePreviews {
+
+  INSTANCE;
+
+  private Map<String, List<NamedStreamPipesEntity>> activePreviews;
+
+  ActivePipelinePreviews() {
+    this.activePreviews = new HashMap<>();
+  }
+
+  public void addActivePreview(String previewId,
+                               List<NamedStreamPipesEntity> activePreviews) {
+    this.activePreviews.put(previewId, activePreviews);
+  }
+
+  public void removePreview(String previewId) {
+    this.activePreviews.remove(previewId);
+  }
+
+  public List<NamedStreamPipesEntity> getInvocationGraphs(String previewId) {
+    return this.activePreviews.get(previewId);
+  }
+
+  public Optional<NamedStreamPipesEntity> getInvocationGraphForPipelineELement(String previewId,
+                                                       String pipelineElementDomId) {
+    List<NamedStreamPipesEntity> graphs = this.activePreviews.get(previewId);
+
+    if (graphs == null || graphs.size() == 0) {
+      return Optional.empty();
+    } else {
+      return graphs
+              .stream()
+              .filter(g -> g.getDOM().equals(pipelineElementDomId))
+              .findFirst();
+    }
+  }
+}
diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/preview/PipelinePreview.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/preview/PipelinePreview.java
new file mode 100644
index 0000000..ddc7bd3
--- /dev/null
+++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/preview/PipelinePreview.java
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ *
+ */
+package org.apache.streampipes.manager.preview;
+
+import org.apache.streampipes.manager.data.PipelineGraph;
+import org.apache.streampipes.manager.data.PipelineGraphBuilder;
+import org.apache.streampipes.manager.execution.http.HttpRequestBuilder;
+import org.apache.streampipes.manager.matching.InvocationGraphBuilder;
+import org.apache.streampipes.manager.operations.Operations;
+import org.apache.streampipes.model.SpDataSet;
+import org.apache.streampipes.model.SpDataStream;
+import org.apache.streampipes.model.base.InvocableStreamPipesEntity;
+import org.apache.streampipes.model.base.NamedStreamPipesEntity;
+import org.apache.streampipes.model.graph.DataProcessorInvocation;
+import org.apache.streampipes.model.pipeline.Pipeline;
+import org.apache.streampipes.model.preview.PipelinePreviewModel;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+public class PipelinePreview {
+
+  public PipelinePreviewModel initiatePreview(Pipeline pipeline) {
+    String previewId = generatePreviewId();
+    pipeline.setActions(new ArrayList<>());
+    PipelineGraph pipelineGraph = new PipelineGraphBuilder(pipeline).buildGraph();
+    List<NamedStreamPipesEntity> graphs = new ArrayList<>(new InvocationGraphBuilder(pipelineGraph, previewId).buildGraphs());
+    graphs.addAll(pipeline.getStreams().stream().filter(stream -> !(stream instanceof SpDataSet)).collect(Collectors.toList()));
+
+    invokeGraphs(filter(graphs));
+    storeGraphs(previewId, graphs);
+
+    return makePreviewModel(previewId, graphs);
+  }
+
+  public void deletePreview(String previewId) {
+    List<NamedStreamPipesEntity> graphs = ActivePipelinePreviews.INSTANCE.getInvocationGraphs(previewId);
+    detachGraphs(filter(graphs));
+    deleteGraphs(previewId);
+  }
+
+  public String getPipelineElementPreview(String previewId,
+                                          String pipelineElementDomId) throws IllegalArgumentException {
+    Optional<NamedStreamPipesEntity> graphOpt = ActivePipelinePreviews
+            .INSTANCE
+            .getInvocationGraphForPipelineELement(previewId, pipelineElementDomId);
+
+    if (graphOpt.isPresent()) {
+      NamedStreamPipesEntity graph = graphOpt.get();
+      if (graph instanceof DataProcessorInvocation) {
+        return Operations.getRuntimeInfo(((DataProcessorInvocation) graph).getOutputStream());
+      } else if (graph instanceof SpDataStream) {
+        return Operations.getRuntimeInfo((SpDataStream) graph);
+      } else {
+        throw new IllegalArgumentException("Requested pipeline element is not a data processor");
+      }
+    } else {
+      throw new IllegalArgumentException("Could not find pipeline element");
+    }
+  }
+
+  private void invokeGraphs(List<InvocableStreamPipesEntity> graphs) {
+    graphs.forEach(g -> new HttpRequestBuilder(g, g.getBelongsTo()).invoke());
+  }
+
+  private void detachGraphs(List<InvocableStreamPipesEntity> graphs) {
+    graphs.forEach(g -> new HttpRequestBuilder(g, g.getUri()).detach());
+  }
+
+  private void deleteGraphs(String previewId) {
+    ActivePipelinePreviews.INSTANCE.removePreview(previewId);
+  }
+
+  private void storeGraphs(String previewId,
+                           List<NamedStreamPipesEntity> graphs) {
+    ActivePipelinePreviews.INSTANCE.addActivePreview(previewId, graphs);
+  }
+
+  private String generatePreviewId() {
+    return UUID.randomUUID().toString();
+  }
+
+  private PipelinePreviewModel makePreviewModel(String previewId,
+                                                List<NamedStreamPipesEntity> graphs) {
+    PipelinePreviewModel previewModel = new PipelinePreviewModel();
+    previewModel.setPreviewId(previewId);
+    previewModel.setSupportedPipelineElementDomIds(collectDomIds(graphs));
+
+    return previewModel;
+  }
+
+  private List<String> collectDomIds(List<NamedStreamPipesEntity> graphs) {
+    return graphs
+            .stream()
+            .map(NamedStreamPipesEntity::getDOM)
+            .collect(Collectors.toList());
+  }
+
+  private List<InvocableStreamPipesEntity> filter(List<NamedStreamPipesEntity> graphs) {
+    List<InvocableStreamPipesEntity> dataProcessors = new ArrayList<>();
+    graphs.stream()
+            .filter(g -> g instanceof DataProcessorInvocation)
+            .forEach(p -> dataProcessors.add((DataProcessorInvocation) p));
+
+    return dataProcessors;
+  }
+}
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineElementPreview.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineElementPreview.java
new file mode 100644
index 0000000..1b21aa2
--- /dev/null
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineElementPreview.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ *
+ */
+package org.apache.streampipes.rest.impl;
+
+import org.apache.streampipes.manager.preview.PipelinePreview;
+import org.apache.streampipes.model.pipeline.Pipeline;
+import org.apache.streampipes.model.preview.PipelinePreviewModel;
+import org.apache.streampipes.rest.shared.annotation.JacksonSerialized;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Path("/v2/users/{username}/pipeline-element-preview")
+public class PipelineElementPreview extends AbstractRestResource {
+
+
+  @POST
+  @Produces(MediaType.APPLICATION_JSON)
+  @Consumes(MediaType.APPLICATION_JSON)
+  @JacksonSerialized
+  public Response requestPipelinePreview(Pipeline pipeline) {
+    PipelinePreviewModel previewModel = new PipelinePreview().initiatePreview(pipeline);
+
+    return ok(previewModel);
+  }
+
+  @GET
+  @Path("{previewId}/{pipelineElementDomId}")
+  @Produces(MediaType.APPLICATION_JSON)
+  @Consumes(MediaType.APPLICATION_JSON)
+  public Response getPipelinePreviewResult(@PathParam("previewId") String previewId,
+                                           @PathParam("pipelineElementDomId") String pipelineElementDomId) {
+    try {
+      String runtimeInfo = new PipelinePreview().getPipelineElementPreview(previewId, pipelineElementDomId);
+      return ok(runtimeInfo);
+    } catch (IllegalArgumentException e) {
+      return badRequest();
+    }
+  }
+
+  @DELETE
+  @Path("{previewId}")
+  @Produces(MediaType.APPLICATION_JSON)
+  public void deletePipelinePreviewRequest(@PathParam("previewId") String previewId) {
+    new PipelinePreview().deletePreview(previewId);
+  }
+
+}
diff --git a/ui/src/app/core-model/gen/streampipes-model.ts b/ui/src/app/core-model/gen/streampipes-model.ts
index 084b63e..aa51aee 100644
--- a/ui/src/app/core-model/gen/streampipes-model.ts
+++ b/ui/src/app/core-model/gen/streampipes-model.ts
@@ -16,10 +16,11 @@
  *
  */
 
+
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 2.27.744 on 2021-05-14 21:55:06.
+// Generated using typescript-generator version 2.27.744 on 2021-05-19 14:58:04.
 
 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 [...]
@@ -189,9 +190,9 @@ export class AdapterDescription extends NamedStreamPipesEntity {
         instance.rules = __getCopyArrayFn(TransformationRuleDescription.fromDataUnion)(data.rules);
         instance.category = __getCopyArrayFn(__identity<string>())(data.category);
         instance.createdAt = data.createdAt;
-        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;
@@ -2292,6 +2293,21 @@ export class PipelineOperationStatus {
     }
 }
 
+export class PipelinePreviewModel {
+    previewId: string;
+    supportedPipelineElementDomIds: string[];
+
+    static fromData(data: PipelinePreviewModel, target?: PipelinePreviewModel): PipelinePreviewModel {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new PipelinePreviewModel();
+        instance.previewId = data.previewId;
+        instance.supportedPipelineElementDomIds = __getCopyArrayFn(__identity<string>())(data.supportedPipelineElementDomIds);
+        return instance;
+    }
+}
+
 export class PipelineStatusMessage {
     message: string;
     messageType: string;
@@ -2663,8 +2679,8 @@ export class SpDataSet extends SpDataStream {
         instance.supportedGrounding = EventGrounding.fromData(data.supportedGrounding);
         instance.datasetInvocationId = data.datasetInvocationId;
         instance.correspondingPipeline = data.correspondingPipeline;
-        instance.actualTopicName = data.actualTopicName;
         instance.brokerHostname = data.brokerHostname;
+        instance.actualTopicName = data.actualTopicName;
         return instance;
     }
 }
diff --git a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html
index ce6c419..c41b62c 100644
--- a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html
+++ b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html
@@ -22,8 +22,12 @@
             <button mat-button matTooltip="Save Pipeline" [matTooltipPosition]="'above'"
                     [disabled]="!PipelineValidationService.pipelineValid"
                     (click)="submit()" type="submit">
-                <i class="material-icons">save</i>&nbsp;<span>Save pipeline</span>
+                <div fxLayoutAlign="start center" fxLayout="row">
+                    <i class="material-icons">save</i>&nbsp;
+                    <span>&nbsp;Save pipeline</span>
+                </div>
             </button>
+            <span class="assembly-options-divider"></span>
             <!-- TODO: Use this once copying of elements is supported -->
 <!--            <button mat-button mat-icon-button matTooltip="Pan" [matTooltipPosition]="'above'"-->
 <!--                    [disabled]="!selectMode"-->
@@ -35,6 +39,15 @@
 <!--                    (click)="toggleSelectMode()">-->
 <!--                <i class="material-icons">mode_edit</i>-->
 <!--            </button>-->
+            <button mat-button matTooltip="Data Preview" [matTooltipPosition]="'above'"
+                    (click)="triggerPipelinePreview()">
+                <div fxLayoutAlign="start center" fxLayout="row">
+                    <i class="material-icons">visibility</i>
+                    <span *ngIf="!pipelineComponent.previewModeActive">&nbsp;Enable live preview</span>
+                    <span *ngIf="pipelineComponent.previewModeActive">&nbsp;Disable live preview</span>
+                </div>
+            </button>
+            <span class="assembly-options-divider"></span>
             <button mat-button mat-icon-button matTooltip="Auto Layout" [matTooltipPosition]="'above'"
                     (click)="autoLayout()">
                 <i class="material-icons">settings_overscan</i>
@@ -125,7 +138,8 @@
                 </div>
             </div>
             <div id="assembly" class="canvas" #assembly>
-                <pipeline [pipelineValid]="pipelineValid"
+                <pipeline #pipelineComponent
+                          [pipelineValid]="pipelineValid"
                           [canvasId]="'assembly'"
                           [rawPipelineModel]="rawPipelineModel"
                           [allElements]="allElements"
diff --git a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.scss b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.scss
index 7629335..b84c217 100644
--- a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.scss
+++ b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.scss
@@ -65,7 +65,9 @@
 }
 
 .pipeline-assembly-options {
+    padding-left: 5px;
     color: white;
+    //background: #f6f6f6;
 }
 
 .jtk-surface-nopan {
@@ -147,3 +149,11 @@
     left: 28px;
     top: 30px;
 }
+
+.assembly-options-divider {
+    width: 2px;
+    height: 70%;
+    margin-left: 10px;
+    margin-right: 10px;
+    background: #b8b8b8;
+}
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 385d5c3..d4a0f68 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
@@ -44,6 +44,7 @@ import {PipelineCanvasScrollingService} from "../../services/pipeline-canvas-scr
 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";
 
 
 @Component({
@@ -86,6 +87,10 @@ export class PipelineAssemblyComponent implements OnInit {
 
     config: any = {};
     @ViewChild("outerCanvas") pipelineCanvas: ElementRef;
+
+    @ViewChild("pipelineComponent")
+    pipelineComponent: PipelineComponent;
+
     panzoom: PanzoomObject;
 
     constructor(private JsPlumbFactoryService: JsplumbFactoryService,
@@ -279,7 +284,6 @@ export class PipelineAssemblyComponent implements OnInit {
     }
 
     panRight() {
-        console.log("panning right");
         this.pan(-100, 0);
     }
 
@@ -306,4 +310,8 @@ export class PipelineAssemblyComponent implements OnInit {
         this.panzoom.pan(x, y);
     }
 
+    triggerPipelinePreview() {
+        this.pipelineComponent.initiatePipelineElementPreview();
+    }
+
 }
diff --git a/ui/src/app/editor/components/pipeline-element-options/pipeline-element-options.component.html b/ui/src/app/editor/components/pipeline-element-options/pipeline-element-options.component.html
index 976a382..8b022fb 100644
--- a/ui/src/app/editor/components/pipeline-element-options/pipeline-element-options.component.html
+++ b/ui/src/app/editor/components/pipeline-element-options/pipeline-element-options.component.html
@@ -42,7 +42,7 @@
             <button mat-button mat-icon-button matTooltip="Compatible Elements" [matTooltipPosition]="'below'"
                     [disabled]="!possibleElements || possibleElements.length == 0 || !isConfigured()"
                     (click)="openPossibleElementsDialog()">
-                <i class="material-icons">visibility</i>
+                <i class="material-icons">account_tree</i>
         </button>
         </span>
         <span class="options-button recommended-button"
@@ -56,7 +56,8 @@
         </span>
         <span class="options-button help-button" style="z-index:10">
             <button matTooltip="Help" [matTooltipPosition]="'below'"
-                    mat-button mat-icon-button (click)="openHelpDialog()">?
+                    mat-button mat-icon-button (click)="openHelpDialog()">
+                <i class="material-icons">help</i>
                 </button>
         </span>
         <div class="editor-pe-info" [ngClass]="'pe-info-' + pipelineElementCssType">
@@ -67,4 +68,4 @@
                                  [pipelineElementDomId]="pipelineElement.payload.dom"
                                  [recommendedElements]="recommendedElements"
                                  [recommendationsShown]="recommendationsShown" *ngIf="recommendationsAvailable"></pipeline-element-recommendation>
-</div>
\ No newline at end of file
+</div>
diff --git a/ui/src/app/editor/components/pipeline-element-preview/pipeline-element-preview.component.html b/ui/src/app/editor/components/pipeline-element-preview/pipeline-element-preview.component.html
new file mode 100644
index 0000000..7a9c5ab
--- /dev/null
+++ b/ui/src/app/editor/components/pipeline-element-preview/pipeline-element-preview.component.html
@@ -0,0 +1,32 @@
+<!--
+  ~ 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="data-preview">
+    <div *ngIf="!runtimeData" fxFlex="100" fxLayout="column" fxLayoutAlign="center center">
+        <mat-spinner [diameter]="20"></mat-spinner>
+        <span class="preview-table mt-10">Waiting for live data...</span>
+    </div>
+    <table class="dataTable row-border hover preview-table" *ngIf="runtimeData">
+        <tbody id="preview-data-rows-id">
+        <tr *ngFor="let item of runtimeData | keyvalue">
+            <td>{{item.key}}</td>
+            <td><b>{{item.value}}</b></td>
+        </tr>
+        </tbody>
+    </table>
+</div>
diff --git a/ui/src/app/editor/editor.component.scss b/ui/src/app/editor/components/pipeline-element-preview/pipeline-element-preview.component.scss
similarity index 73%
copy from ui/src/app/editor/editor.component.scss
copy to ui/src/app/editor/components/pipeline-element-preview/pipeline-element-preview.component.scss
index 8aeccf2..c201870 100644
--- a/ui/src/app/editor/editor.component.scss
+++ b/ui/src/app/editor/components/pipeline-element-preview/pipeline-element-preview.component.scss
@@ -16,20 +16,23 @@
  *
  */
 
-@import '../../scss/variables';
-
-.text-color {
-  color: $sp-color-accent;
-}
-
-.page-container {
-  border: 0;
+.data-preview {
+  position: relative;
+  left: 0px;
+  top: 120px;
+  width: 250px;
+  height: 120px;
+  background: white;
+  overflow: auto;
+  border: 1px solid gray;
+  box-shadow: 0.175em 0.175em 0 0 rgba(15, 28, 63, 0.125);
+  z-index:50;
 }
 
-.border {
-  border: 1px solid #cccccc;
+.preview-table {
+  font-size:9pt;
 }
 
-.icon-stand-margin {
-  margin-bottom: 10px;
+.mt-10 {
+  margin-top: 10px;
 }
diff --git a/ui/src/app/editor/components/pipeline-element-preview/pipeline-element-preview.component.ts b/ui/src/app/editor/components/pipeline-element-preview/pipeline-element-preview.component.ts
new file mode 100644
index 0000000..1d68604
--- /dev/null
+++ b/ui/src/app/editor/components/pipeline-element-preview/pipeline-element-preview.component.ts
@@ -0,0 +1,68 @@
+/*
+ * 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, OnDestroy, OnInit} from "@angular/core";
+import {EditorService} from "../../services/editor.service";
+
+@Component({
+  selector: 'pipeline-element-preview',
+  templateUrl: './pipeline-element-preview.component.html',
+  styleUrls: ['./pipeline-element-preview.component.scss']
+})
+export class PipelineElementPreviewComponent implements OnInit, OnDestroy {
+
+  @Input()
+  previewId: string;
+
+  @Input()
+  pipelineElementDomId: string;
+
+  runtimeData: ReadonlyMap<string, unknown>;
+
+  runtimeDataError: boolean = false;
+  timer: any;
+
+  constructor(private editorService: EditorService) {
+
+  }
+
+  ngOnInit(): void {
+    this.getLatestRuntimeInfo();
+  }
+
+  ngOnDestroy(): void {
+  }
+
+  getLatestRuntimeInfo() {
+    this.editorService.getPipelinePreviewResult(this.previewId, this.pipelineElementDomId).subscribe(data => {
+      if (data) {
+        this.runtimeDataError = false;
+        if (!(Object.keys(data).length === 0 && data.constructor === Object)) {
+          this.runtimeData = data;
+        }
+
+        this.timer = setTimeout(() => {
+          this.getLatestRuntimeInfo();
+        }, 1000);
+      } else {
+        this.runtimeDataError = true;
+      }
+    });
+  }
+
+}
diff --git a/ui/src/app/editor/components/pipeline/pipeline.component.html b/ui/src/app/editor/components/pipeline/pipeline.component.html
index b3736fa..603b15e 100644
--- a/ui/src/app/editor/components/pipeline/pipeline.component.html
+++ b/ui/src/app/editor/components/pipeline/pipeline.component.html
@@ -27,8 +27,10 @@
             <div class="pipeline-element-progress-container sp-fade" *ngIf="pipelineElement.settings.loadingStatus">
                  <mat-spinner [mode]="'indeterminate'" class="pipeline-element-progress" [diameter]="40"></mat-spinner>
             </div>
-            <div class="pipeline-element-loading-container sp-fade-opacity" *ngIf="pipelineElement.settings.loadingStatus"></div>
-            <div class="pipeline-element-configuration-invalid {{pipelineElement.type === 'stream' ? 'pi-stream' : 'pi-processor'}}" *ngIf="!pipelineElement.settings.completed">
+            <div class="pipeline-element-loading-container sp-fade-opacity"
+                 *ngIf="pipelineElement.settings.loadingStatus"></div>
+            <div class="pipeline-element-configuration-invalid {{pipelineElement.type === 'stream' ? 'pi-stream' : 'pi-processor'}}"
+                 *ngIf="!pipelineElement.settings.completed">
                 <i class="material-icons pipeline-element-configuration-invalid-icon">
                 warning
                 </i>
@@ -45,6 +47,11 @@
                                    [pipelineElementId]="pipelineElement.type == 'stream' ? pipelineElement.payload.elementId : pipelineElement.payload.belongsTo"
                                    [internalId]="pipelineElement.payload.dom">
         </pipeline-element-options>
+        <pipeline-element-preview *ngIf="previewModeActive && (pipelinePreview.supportedPipelineElementDomIds.indexOf(pipelineElement.payload.dom) > -1)"
+                                  [previewId]="pipelinePreview.previewId"
+                                  [pipelineElementDomId]="pipelineElement.payload.dom">
+
+        </pipeline-element-preview>
     </span>
 </div>
 
diff --git a/ui/src/app/editor/components/pipeline/pipeline.component.ts b/ui/src/app/editor/components/pipeline/pipeline.component.ts
index 22ad8c0..e9b8bc7 100644
--- a/ui/src/app/editor/components/pipeline/pipeline.component.ts
+++ b/ui/src/app/editor/components/pipeline/pipeline.component.ts
@@ -38,7 +38,7 @@ import {
 import {
   CustomOutputStrategy,
   DataProcessorInvocation, DataSinkInvocation, ErrorMessage,
-  Pipeline, SpDataSet,
+  Pipeline, PipelinePreviewModel, SpDataSet,
   SpDataStream, SpDataStreamUnion
 } from "../../../core-model/gen/streampipes-model";
 import {ObjectProvider} from "../../services/object-provider.service";
@@ -104,6 +104,9 @@ export class PipelineComponent implements OnInit, OnDestroy {
 
   JsplumbBridge: JsplumbBridge;
 
+  previewModeActive: boolean = false;
+  pipelinePreview: PipelinePreviewModel;
+
   constructor(private JsplumbService: JsplumbService,
               private PipelineEditorService: PipelineEditorService,
               private JsplumbFactoryService: JsplumbFactoryService,
@@ -113,7 +116,7 @@ export class PipelineComponent implements OnInit, OnDestroy {
               private PipelineValidationService: PipelineValidationService,
               private dialogService: DialogService,
               private dialog: MatDialog,
-              private ngZone: NgZone) {
+              private ngZone: NgZone,) {
     this.plumbReady = false;
     this.currentMouseOverElement = "";
     this.currentPipelineModel = new Pipeline();
@@ -140,6 +143,7 @@ export class PipelineComponent implements OnInit, OnDestroy {
   }
 
   ngOnDestroy() {
+    this.deletePipelineElementPreview(false);
     this.JsplumbBridge.deleteEveryEndpoint();
     this.plumbReady = false;
   }
@@ -220,20 +224,19 @@ export class PipelineComponent implements OnInit, OnDestroy {
                   this.JsplumbService.dataStreamDropped(pipelineElementConfig.payload.dom, pipelineElementConfig.payload as SpDataSet, true, false);
                 });
               }, 0);
-            }
-            else if (ui.draggable.hasClass('stream')) {
+            } else if (ui.draggable.hasClass('stream')) {
               this.checkTopicModel(pipelineElementConfig);
             } else if (ui.draggable.hasClass('sepa')) {
-                setTimeout(() => {
-                  this.JsplumbService.dataProcessorDropped(pipelineElementConfig.payload.dom, pipelineElementConfig.payload as DataProcessorInvocation, true, false);
-                }, 10);
+              setTimeout(() => {
+                this.JsplumbService.dataProcessorDropped(pipelineElementConfig.payload.dom, pipelineElementConfig.payload as DataProcessorInvocation, true, false);
+              }, 10);
             } else if (ui.draggable.hasClass('action')) {
-                setTimeout(() => {
-                  this.JsplumbService.dataSinkDropped(pipelineElementConfig.payload.dom, pipelineElementConfig.payload as DataSinkInvocation, true, false);
-                }, 10);
+              setTimeout(() => {
+                this.JsplumbService.dataSinkDropped(pipelineElementConfig.payload.dom, pipelineElementConfig.payload as DataSinkInvocation, true, false);
+              }, 10);
             }
             if (this.ShepherdService.isTourActive()) {
-              this.ShepherdService.trigger("drop-" +pipelineElementConfig.type);
+              this.ShepherdService.trigger("drop-" + pipelineElementConfig.type);
             }
           }
         }
@@ -246,12 +249,12 @@ export class PipelineComponent implements OnInit, OnDestroy {
   }
 
   checkTopicModel(pipelineElementConfig: PipelineElementConfig) {
-      setTimeout(() => {
-        this.JsplumbService.dataStreamDropped(pipelineElementConfig.payload.dom,
-            pipelineElementConfig.payload as SpDataStream,
-            true,
-            false);
-      }, 10);
+    setTimeout(() => {
+      this.JsplumbService.dataStreamDropped(pipelineElementConfig.payload.dom,
+          pipelineElementConfig.payload as SpDataStream,
+          true,
+          false);
+    }, 10);
 
     var streamDescription = pipelineElementConfig.payload as SpDataStream;
     if (streamDescription
@@ -414,7 +417,7 @@ export class PipelineComponent implements OnInit, OnDestroy {
   }
 
   showCustomizeDialog(pipelineElementInfo: Tuple2<Boolean, PipelineElementConfig>) {
-    const dialogRef = this.dialogService.open(CustomizeComponent,{
+    const dialogRef = this.dialogService.open(CustomizeComponent, {
       panelType: PanelType.SLIDE_IN_PANEL,
       title: "Customize " + pipelineElementInfo.b.payload.name,
       width: "50vw",
@@ -434,6 +437,9 @@ export class PipelineComponent implements OnInit, OnDestroy {
         this.JsplumbBridge.getSourceEndpoint(pipelineElementInfo.b.payload.dom).setType("token");
         this.triggerPipelineCacheUpdate();
         this.announceConfiguredElement(pipelineElementInfo.b);
+        if (this.previewModeActive) {
+          this.deletePipelineElementPreview(true);
+        }
       }
       this.validatePipeline();
     });
@@ -443,5 +449,26 @@ export class PipelineComponent implements OnInit, OnDestroy {
     this.EditorService.announceConfiguredElement(pe.payload.dom);
   }
 
+  initiatePipelineElementPreview() {
+    if (!this.previewModeActive) {
+      let pipeline = this.ObjectProvider.makePipeline(this.rawPipelineModel);
+      this.EditorService.initiatePipelinePreview(pipeline).subscribe(response => {
+        this.pipelinePreview = response;
+        this.previewModeActive = true;
+      });
+    } else {
+      this.deletePipelineElementPreview(false);
+    }
+  }
 
+  deletePipelineElementPreview(resume: boolean) {
+    if (this.previewModeActive) {
+      this.EditorService.deletePipelinePreviewRequest(this.pipelinePreview.previewId).subscribe(response => {
+        this.previewModeActive = false;
+        if (resume) {
+          this.initiatePipelineElementPreview();
+        }
+      });
+    }
+  }
 }
diff --git a/ui/src/app/editor/editor.component.html b/ui/src/app/editor/editor.component.html
index 5d47715..cf750ae 100644
--- a/ui/src/app/editor/editor.component.html
+++ b/ui/src/app/editor/editor.component.html
@@ -42,13 +42,13 @@
         <div id="shepherd-test"
              style="background-color:#f6f6f6;padding:0px;border-bottom:1px solid #ffffff">
             <pipeline-element-icon-stand
-                    class="icon-stand-margin"
                     [activeType]="activeType"
                     [currentElements]="currentElements"
                     *ngIf="allElementsLoaded && !pipelineCanvasMaximized">
             </pipeline-element-icon-stand>
         </div>
         <pipeline-assembly fxFlex="1"
+                           class="pipeline-assembly-margin"
                            [rawPipelineModel]="rawPipelineModel"
                            [allElements]="allElements"
                            [currentModifiedPipelineId]="currentModifiedPipelineId"
diff --git a/ui/src/app/editor/editor.component.scss b/ui/src/app/editor/editor.component.scss
index 8aeccf2..b8cc5df 100644
--- a/ui/src/app/editor/editor.component.scss
+++ b/ui/src/app/editor/editor.component.scss
@@ -30,6 +30,6 @@
   border: 1px solid #cccccc;
 }
 
-.icon-stand-margin {
-  margin-bottom: 10px;
+.pipeline-assembly-margin {
+  margin-top: 10px;
 }
diff --git a/ui/src/app/editor/editor.module.ts b/ui/src/app/editor/editor.module.ts
index b5c10e0..da8f5e0 100644
--- a/ui/src/app/editor/editor.module.ts
+++ b/ui/src/app/editor/editor.module.ts
@@ -65,6 +65,7 @@ import {PipelineElementDraggedService} from "./services/pipeline-element-dragged
 import {PipelineCanvasScrollingService} from "./services/pipeline-canvas-scrolling.service";
 import {JsplumbEndpointService} from "./services/jsplumb-endpoint.service";
 import {JsplumbFactoryService} from "./services/jsplumb-factory.service";
+import {PipelineElementPreviewComponent} from "./components/pipeline-element-preview/pipeline-element-preview.component";
 
 @NgModule({
     imports: [
@@ -99,6 +100,7 @@ import {JsplumbFactoryService} from "./services/jsplumb-factory.service";
         PipelineElementDocumentationComponent,
         PipelineElementIconStandComponent,
         PipelineElementOptionsComponent,
+        PipelineElementPreviewComponent,
         PipelineElementRecommendationComponent,
         PipelineElementTemplateConfigComponent,
         PipelineComponent,
diff --git a/ui/src/app/editor/services/editor.service.ts b/ui/src/app/editor/services/editor.service.ts
index 4ec4b83..18b115d 100644
--- a/ui/src/app/editor/services/editor.service.ts
+++ b/ui/src/app/editor/services/editor.service.ts
@@ -21,9 +21,9 @@ import {HttpClient} from "@angular/common/http";
 import {
   DataProcessorInvocation,
   DataSetModificationMessage,
-  DataSinkInvocation,
+  DataSinkInvocation, Pipeline,
   PipelineElementRecommendationMessage,
-  PipelineModificationMessage,
+  PipelineModificationMessage, PipelinePreviewModel,
   SpDataSet,
   SpDataStream
 } from "../../core-model/gen/streampipes-model";
@@ -141,4 +141,24 @@ export class EditorService {
             }
         });
     }
-}
\ No newline at end of file
+
+    initiatePipelinePreview(pipeline: Pipeline): Observable<PipelinePreviewModel> {
+      return this.http.post(this.pipelinePreviewBasePath, pipeline)
+          .pipe(map(response => PipelinePreviewModel.fromData(response as any)));
+    }
+
+  deletePipelinePreviewRequest(previewId: string): Observable<any> {
+      return this.http.delete(this.pipelinePreviewBasePath + "/" + previewId);
+  }
+
+  getPipelinePreviewResult(previewId: string, pipelineElementDomId: string): Observable<any> {
+      return this.http.get(this.pipelinePreviewBasePath
+          + "/"
+          + previewId
+          + "/" + pipelineElementDomId, {headers: { ignoreLoadingBar: '' }});
+  }
+
+  get pipelinePreviewBasePath() {
+      return this.platformServicesCommons.authUserBasePath() + "/pipeline-element-preview";
+  }
+}
diff --git a/ui/src/scss/sp/pipeline-element-options.scss b/ui/src/scss/sp/pipeline-element-options.scss
index e63c5dc..a61d505 100644
--- a/ui/src/scss/sp/pipeline-element-options.scss
+++ b/ui/src/scss/sp/pipeline-element-options.scss
@@ -25,20 +25,24 @@
   width: 40%;
   border-radius: 50%;
   box-shadow: 0 0 3px $sp-color-accent;
-  line-height: 1.6;
   text-align: center;
-  padding: 2px 0;
+  padding: 1px 1px;
   background-color: $sp-color-accent;
   color: rgba(255, 255, 255, 0.75);
 }
 
 .customize-button {
-  left: 5%;
+  left: -20%;
+  top: -10%;
+}
+
+.preview-button {
+  left: 25%;
   top: -10%;
 }
 
 .delete-button {
-  left: 55%;
+  left: 70%;
   top: -10%;
   background-color: darkred;
   box-shadow: 0 0 3px darkred;
@@ -119,4 +123,4 @@
 .pe-info-set {
   background: $sp-color-set;
   color: white;
-}
\ No newline at end of file
+}