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 2022/08/23 08:08:45 UTC

[incubator-streampipes] 02/02: [STREAMPIPES-579] Fetch dashboard data in a single request

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

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

commit 324d15384603a2aa5beeb78616002007e8be835f
Author: Dominik Riemer <do...@gmail.com>
AuthorDate: Tue Aug 23 10:08:33 2022 +0200

    [STREAMPIPES-579] Fetch dashboard data in a single request
---
 .../dataexplorer/DataLakeManagementV4.java         |   9 +-
 .../dataexplorer/v4/query/DataExplorerQueryV4.java |  15 ++
 .../streampipes/model/datalake/SpQueryResult.java  |   9 +
 .../apache/streampipes/ps/DataLakeResourceV4.java  |  15 ++
 .../src/lib/apis/datalake-rest.service.ts          |   5 +
 .../lib/model/datalake/DatalakeQueryParameters.ts  |   3 +
 .../src/lib/model/gen/streampipes-model.ts         |  13 +-
 .../components/grid/dashboard-grid.component.html  |   4 +-
 .../components/grid/dashboard-grid.component.ts    | 187 ++++++++++++++-------
 .../panel/dashboard-panel.component.html           |   3 +-
 .../components/panel/dashboard-panel.component.ts  |  14 +-
 .../widget/dashboard-widget.component.html         |  42 +++--
 .../widget/dashboard-widget.component.ts           |  46 ++++-
 .../components/widgets/base/base-widget.ts         |  42 +++--
 .../edit-dashboard-dialog.component.html           |  11 ++
 15 files changed, 311 insertions(+), 107 deletions(-)

diff --git a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataLakeManagementV4.java b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataLakeManagementV4.java
index 8f6a858ff..b58ebf5c5 100644
--- a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataLakeManagementV4.java
+++ b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataLakeManagementV4.java
@@ -66,6 +66,8 @@ import static org.apache.streampipes.dataexplorer.v4.SupportedDataLakeQueryParam
 
 public class DataLakeManagementV4 {
 
+    public static final String FOR_ID_KEY = "forId";
+
     private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
             .appendPattern("uuuu[-MM[-dd]]['T'HH[:mm[:ss[.SSSSSSSSS][.SSSSSSSS][.SSSSSSS][.SSSSSS][.SSSSS][.SSSS][.SSS][.SS][.S]]]][XXX]")
             .parseDefaulting(ChronoField.NANO_OF_SECOND, 0)
@@ -87,7 +89,12 @@ public class DataLakeManagementV4 {
             return new DataExplorerQueryV4(queryParts, maximumAmountOfEvents).executeQuery();
         }
 
-        return new DataExplorerQueryV4(queryParts).executeQuery();
+        if (queryParams.getProvidedParams().containsKey(FOR_ID_KEY)) {
+            String forWidgetId = queryParams.getProvidedParams().get(FOR_ID_KEY);
+            return new DataExplorerQueryV4(queryParts, forWidgetId).executeQuery();
+        } else {
+            return new DataExplorerQueryV4(queryParts).executeQuery();
+        }
     }
 
     public void getDataAsStream(ProvidedQueryParams params, String format, OutputStream outputStream) throws IOException {
diff --git a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/v4/query/DataExplorerQueryV4.java b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/v4/query/DataExplorerQueryV4.java
index a26c4b357..30a87d483 100644
--- a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/v4/query/DataExplorerQueryV4.java
+++ b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/v4/query/DataExplorerQueryV4.java
@@ -44,10 +44,20 @@ public class DataExplorerQueryV4 {
 
     protected int maximumAmountOfEvents;
 
+    private boolean appendId = false;
+    private String forId;
+
     public DataExplorerQueryV4() {
 
     }
 
+    public DataExplorerQueryV4(Map<String, QueryParamsV4> params,
+                               String forId) {
+        this(params);
+        this.appendId = true;
+        this.forId = forId;
+    }
+
     public DataExplorerQueryV4(Map<String, QueryParamsV4> params) {
         this.params = params;
         this.maximumAmountOfEvents = -1;
@@ -134,6 +144,11 @@ public class DataExplorerQueryV4 {
                 result.addDataResult(series);
             });
         }
+
+        if (this.appendId) {
+            result.setForId(this.forId);
+        }
+
         return result;
     }
 
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/SpQueryResult.java b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/SpQueryResult.java
index 8a2a54c8b..c3c9ef7e8 100644
--- a/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/SpQueryResult.java
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/SpQueryResult.java
@@ -31,6 +31,7 @@ public class SpQueryResult {
     private List<DataSeries> allDataSeries;
     private int sourceIndex;
     private SpQueryStatus spQueryStatus;
+    private String forId;
 
     public SpQueryResult() {
         this.total = 0;
@@ -92,4 +93,12 @@ public class SpQueryResult {
     public void setSpQueryStatus(SpQueryStatus spQueryStatus) {
         this.spQueryStatus = spQueryStatus;
     }
+
+    public String getForId() {
+        return forId;
+    }
+
+    public void setForId(String forId) {
+        this.forId = forId;
+    }
 }
diff --git a/streampipes-platform-services/src/main/java/org/apache/streampipes/ps/DataLakeResourceV4.java b/streampipes-platform-services/src/main/java/org/apache/streampipes/ps/DataLakeResourceV4.java
index 92397a221..f825ae622 100644
--- a/streampipes-platform-services/src/main/java/org/apache/streampipes/ps/DataLakeResourceV4.java
+++ b/streampipes-platform-services/src/main/java/org/apache/streampipes/ps/DataLakeResourceV4.java
@@ -40,6 +40,7 @@ import javax.ws.rs.core.*;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 import static org.apache.streampipes.dataexplorer.v4.SupportedDataLakeQueryParameters.*;
 
@@ -165,6 +166,20 @@ public class DataLakeResourceV4 extends AbstractRestResource {
         }
     }
 
+    @POST
+    @Path("/query")
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response getData(List<Map<String, String>> queryParams) {
+        var results = queryParams
+          .stream()
+          .map(qp -> new ProvidedQueryParams(qp.get("measureName"), qp))
+          .map(params -> this.dataLakeManagement.getData(params))
+          .collect(Collectors.toList());
+
+        return ok(results);
+    }
+
     @GET
     @Path("/measurements/{measurementID}/download")
     @Produces(MediaType.APPLICATION_OCTET_STREAM)
diff --git a/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts b/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts
index f83c5a2d7..6bcd51196 100644
--- a/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts
+++ b/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts
@@ -45,6 +45,11 @@ export class DatalakeRestService {
     }));
   }
 
+  performMultiQuery(queryParams: DatalakeQueryParameters[]): Observable<SpQueryResult[]> {
+    return this.http.post(`${this.dataLakeUrl}/query`, queryParams, {headers: {ignoreLoadingBar: ''}})
+      .pipe(map(response => response as SpQueryResult[]));
+  }
+
   getData(index: string,
           queryParams: DatalakeQueryParameters,
           ignoreLoadingBar?: boolean): Observable<SpQueryResult> {
diff --git a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts b/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
index 3cfd937a5..d69493548 100644
--- a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
+++ b/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
@@ -32,5 +32,8 @@ export class DatalakeQueryParameters {
   public filter: string;
   public maximumAmountOfEvents: number;
 
+  // should be only used for multi-query requests
+  public measureName: string;
+  public forId: string;
 }
 
diff --git a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
index 2708db878..b6e2425c5 100644
--- a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
+++ b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
@@ -15,10 +15,11 @@
  *   limitations under the License.
  */
 
+
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 2.27.744 on 2022-08-19 14:38:41.
+// Generated using typescript-generator version 2.27.744 on 2022-08-23 09:28:28.
 
 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 [...]
@@ -192,8 +193,8 @@ export class AdapterDescription extends NamedStreamPipesEntity {
         instance.selectedEndpointUrl = data.selectedEndpointUrl;
         instance.correspondingServiceGroup = data.correspondingServiceGroup;
         instance.correspondingDataStreamElementId = data.correspondingDataStreamElementId;
-        instance.streamRules = __getCopyArrayFn(TransformationRuleDescription.fromDataUnion)(data.streamRules);
         instance.valueRules = __getCopyArrayFn(TransformationRuleDescription.fromDataUnion)(data.valueRules);
+        instance.streamRules = __getCopyArrayFn(TransformationRuleDescription.fromDataUnion)(data.streamRules);
         instance.schemaRules = __getCopyArrayFn(TransformationRuleDescription.fromDataUnion)(data.schemaRules);
         return instance;
     }
@@ -1790,9 +1791,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;
     }
 }
@@ -1809,9 +1810,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;
     }
 }
@@ -2641,9 +2642,9 @@ export class PipelineTemplateDescription extends NamedStreamPipesEntity {
         const instance = target || new PipelineTemplateDescription();
         super.fromData(data, instance);
         instance.boundTo = __getCopyArrayFn(BoundPipelineElement.fromData)(data.boundTo);
+        instance.pipelineTemplateId = data.pipelineTemplateId;
         instance.pipelineTemplateDescription = data.pipelineTemplateDescription;
         instance.pipelineTemplateName = data.pipelineTemplateName;
-        instance.pipelineTemplateId = data.pipelineTemplateId;
         return instance;
     }
 }
@@ -3052,6 +3053,7 @@ export class SpDataSet extends SpDataStream {
 
 export class SpQueryResult {
     allDataSeries: DataSeries[];
+    forId: string;
     headers: string[];
     sourceIndex: number;
     spQueryStatus: SpQueryStatus;
@@ -3067,6 +3069,7 @@ export class SpQueryResult {
         instance.allDataSeries = __getCopyArrayFn(DataSeries.fromData)(data.allDataSeries);
         instance.sourceIndex = data.sourceIndex;
         instance.spQueryStatus = data.spQueryStatus;
+        instance.forId = data.forId;
         return instance;
     }
 }
diff --git a/ui/src/app/dashboard/components/grid/dashboard-grid.component.html b/ui/src/app/dashboard/components/grid/dashboard-grid.component.html
index 94baf0cbc..de987ce73 100644
--- a/ui/src/app/dashboard/components/grid/dashboard-grid.component.html
+++ b/ui/src/app/dashboard/components/grid/dashboard-grid.component.html
@@ -23,10 +23,12 @@
 <gridster [options]="options" [ngClass]="editMode ? 'edit' : ''">
     <ng-container *ngFor="let item of dashboard.widgets;let i=index">
         <gridster-item [item]="item" #gridsterItemComponent>
-            <dashboard-widget (updateCallback)="propagateItemUpdate($event)"
+            <dashboard-widget (updateCallback)="propagateItemUpdate($event)" #dashboardWidgetComponent
                               (deleteCallback)="propagateItemRemoval($event)"
                               [widget]="item"
                               [editMode]="editMode"
+                              [allMeasurements]="allMeasurements"
+                              [globalRefresh]="dashboard.dashboardGeneralSettings.globalRefresh"
                               [headerVisible]="headerVisible"
                               [gridsterItemComponent]="gridsterItemComponent"
                               [itemWidth]="gridsterItemComponent.width"
diff --git a/ui/src/app/dashboard/components/grid/dashboard-grid.component.ts b/ui/src/app/dashboard/components/grid/dashboard-grid.component.ts
index 942e80157..443aa4999 100644
--- a/ui/src/app/dashboard/components/grid/dashboard-grid.component.ts
+++ b/ui/src/app/dashboard/components/grid/dashboard-grid.component.ts
@@ -17,88 +17,145 @@
  */
 
 import {
-    Component,
-    EventEmitter,
-    Input,
-    OnChanges,
-    OnInit,
-    Output,
-    QueryList,
-    SimpleChanges,
-    ViewChildren
+  AfterContentInit,
+  Component,
+  EventEmitter,
+  Input,
+  OnChanges, OnDestroy,
+  OnInit,
+  Output,
+  QueryList,
+  SimpleChanges,
+  ViewChildren
 } from '@angular/core';
-import {Dashboard, DashboardConfig, DashboardItem, DashboardWidgetModel} from '@streampipes/platform-services';
-import {ResizeService} from '../../services/resize.service';
-import {GridsterItemComponent, GridType} from 'angular-gridster2';
-import {GridsterInfo} from "../../models/gridster-info.model";
+import {
+  Dashboard,
+  DashboardConfig,
+  DashboardItem,
+  DashboardWidgetModel, DataLakeMeasure,
+  DatalakeRestService, SpQueryResult
+} from '@streampipes/platform-services';
+import { ResizeService } from '../../services/resize.service';
+import { GridsterItemComponent, GridType } from 'angular-gridster2';
+import { GridsterInfo } from "../../models/gridster-info.model";
+import { DashboardWidgetComponent } from '../widget/dashboard-widget.component';
+import { exhaustMap } from 'rxjs/operators';
+import { Observable, of, Subscription, timer } from 'rxjs';
 
 @Component({
-    selector: 'dashboard-grid',
-    templateUrl: './dashboard-grid.component.html',
-    styleUrls: ['./dashboard-grid.component.css']
+  selector: 'dashboard-grid',
+  templateUrl: './dashboard-grid.component.html',
+  styleUrls: ['./dashboard-grid.component.css']
 })
-export class DashboardGridComponent implements OnInit, OnChanges {
+export class DashboardGridComponent implements OnInit, OnChanges, AfterContentInit, OnDestroy {
 
-    @Input() editMode: boolean;
-    @Input() headerVisible: boolean;
-    @Input() dashboard: Dashboard;
+  @Input() editMode: boolean;
+  @Input() headerVisible: boolean;
+  @Input() dashboard: Dashboard;
+  @Input() allMeasurements: DataLakeMeasure[];
 
-    @Output() deleteCallback: EventEmitter<DashboardItem> = new EventEmitter<DashboardItem>();
-    @Output() updateCallback: EventEmitter<DashboardWidgetModel> = new EventEmitter<DashboardWidgetModel>();
+  @Output() deleteCallback: EventEmitter<DashboardItem> = new EventEmitter<DashboardItem>();
+  @Output() updateCallback: EventEmitter<DashboardWidgetModel> = new EventEmitter<DashboardWidgetModel>();
 
-    options: DashboardConfig;
-    loaded = false;
+  options: DashboardConfig;
+  loaded = false;
 
-    @ViewChildren(GridsterItemComponent) gridsterItemComponents: QueryList<GridsterItemComponent>;
+  subscription: Subscription;
 
-    constructor(private resizeService: ResizeService) {
+  @ViewChildren(GridsterItemComponent) gridsterItemComponents: QueryList<GridsterItemComponent>;
+  @ViewChildren(DashboardWidgetComponent) dashboardWidgetComponents: QueryList<DashboardWidgetComponent>;
 
-    }
+  constructor(private resizeService: ResizeService,
+              private datalakeRestService: DatalakeRestService) {
+
+  }
 
-    ngOnInit(): void {
-        this.options = {
-            disablePushOnDrag: true,
-            draggable: {enabled: this.editMode},
-            gridType: GridType.VerticalFixed,
-            minCols: 12,
-            maxCols: 12,
-            minRows: 4,
-            fixedRowHeight: 50,
-            fixedColWidth: 50,
-            margin: 5,
-            resizable: {enabled: this.editMode},
-            displayGrid: this.editMode ? 'always' : 'none',
-            itemResizeCallback: ((item, itemComponent) => {
-                this.resizeService.notify({
-                    gridsterItem: item,
-                    gridsterItemComponent: itemComponent
-                } as GridsterInfo);
-            }),
-            itemInitCallback: ((item, itemComponent) => {
-                this.resizeService.notify({
-                    gridsterItem: item,
-                    gridsterItemComponent: itemComponent
-                } as GridsterInfo);
-                //window.dispatchEvent(new Event('resize'));
-            })
-        };
+  ngOnInit(): void {
+    this.options = {
+      disablePushOnDrag: true,
+      draggable: {enabled: this.editMode},
+      gridType: GridType.VerticalFixed,
+      minCols: 12,
+      maxCols: 12,
+      minRows: 4,
+      fixedRowHeight: 50,
+      fixedColWidth: 50,
+      margin: 5,
+      resizable: {enabled: this.editMode},
+      displayGrid: this.editMode ? 'always' : 'none',
+      itemResizeCallback: ((item, itemComponent) => {
+        this.resizeService.notify({
+          gridsterItem: item,
+          gridsterItemComponent: itemComponent
+        } as GridsterInfo);
+      }),
+      itemInitCallback: ((item, itemComponent) => {
+        this.resizeService.notify({
+          gridsterItem: item,
+          gridsterItemComponent: itemComponent
+        } as GridsterInfo);
+        //window.dispatchEvent(new Event('resize'));
+      })
+    };
+  }
+
+  ngOnDestroy() {
+    if (this.subscription) {
+      this.subscription.unsubscribe();
     }
+  }
 
-    ngOnChanges(changes: SimpleChanges): void {
-        if (changes['editMode'] && this.options) {
-            this.options.draggable.enabled = this.editMode;
-            this.options.resizable.enabled = this.editMode;
-            this.options.displayGrid = this.editMode ? 'always' : 'none';
-            this.options.api.optionsChanged();
-        }
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes['editMode'] && this.options) {
+      this.options.draggable.enabled = this.editMode;
+      this.options.resizable.enabled = this.editMode;
+      this.options.displayGrid = this.editMode ? 'always' : 'none';
+      this.options.api.optionsChanged();
     }
+  }
 
-    propagateItemRemoval(widget: DashboardItem) {
-        this.deleteCallback.emit(widget);
+  propagateItemRemoval(widget: DashboardItem) {
+    this.deleteCallback.emit(widget);
+  }
+
+  propagateItemUpdate(dashboardWidget: DashboardWidgetModel) {
+    this.updateCallback.emit(dashboardWidget);
+  }
+
+  ngAfterContentInit(): void {
+    if (this.dashboard.dashboardGeneralSettings.globalRefresh) {
+      this.checkWidgetsReady();
     }
+  }
 
-    propagateItemUpdate(dashboardWidget: DashboardWidgetModel) {
-        this.updateCallback.emit(dashboardWidget);
+  checkWidgetsReady() {
+    if (this.dashboardWidgetComponents) {
+      this.createQuerySubscription();
+    } else {
+      setTimeout(() => this.checkWidgetsReady(), 1000);
     }
+  }
+
+  createQuerySubscription() {
+    this.subscription = timer(0, this.dashboard.dashboardGeneralSettings.refreshIntervalInSeconds * 1000)
+      .pipe(exhaustMap(() => this.makeQueryObservable()))
+      .subscribe(res => {
+        if (res.length > 0) {
+          this.dashboardWidgetComponents.forEach((widget, index) => {
+            const widgetId = widget.getWidgetId();
+            const queryResult = res.find(r => r.forId === widgetId);
+            if (queryResult) {
+              widget.processQueryResponse(queryResult);
+            }
+          });
+        }
+      });
+  }
 
+  makeQueryObservable(): Observable<SpQueryResult[]> {
+    const queries = this.dashboardWidgetComponents
+      .map(dw => dw.getWidgetQuery())
+      .filter(query => query !== undefined);
+    return this.datalakeRestService.performMultiQuery(queries);
+  }
 }
diff --git a/ui/src/app/dashboard/components/panel/dashboard-panel.component.html b/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
index 2b09d951f..1e368cfb3 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
+++ b/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
@@ -56,9 +56,10 @@
     <div fxFlex="100" fxLayout="column">
         <dashboard-grid [editMode]="editMode" [dashboard]="dashboard"
                         [headerVisible]="headerVisible"
+                        [allMeasurements]="allMeasurements"
                         (updateCallback)="updateAndQueueItemForDeletion($event)"
                         (deleteCallback)="removeAndQueueItemForDeletion($event)"
-                        *ngIf="dashboard"
+                        *ngIf="dashboard && allMeasurements"
                         class="h-100 dashboard-grid"></dashboard-grid>
     </div>
 </sp-basic-view>
diff --git a/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts b/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts
index b244e59ca..11d0cfbb9 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts
+++ b/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts
@@ -17,7 +17,13 @@
  */
 
 import { Component, EventEmitter, OnInit, Output } from '@angular/core';
-import { ClientDashboardItem, Dashboard, DashboardService, DashboardWidgetModel } from '@streampipes/platform-services';
+import {
+    ClientDashboardItem,
+    Dashboard,
+    DashboardService,
+    DashboardWidgetModel, DataLakeMeasure,
+    DatalakeRestService
+} from '@streampipes/platform-services';
 import { forkJoin, Observable, of, Subscription } from 'rxjs';
 import { AddVisualizationDialogComponent } from '../../dialogs/add-widget/add-visualization-dialog.component';
 import { RefreshDashboardService } from '../../services/refresh-dashboard.service';
@@ -46,10 +52,12 @@ export class DashboardPanelComponent implements OnInit {
 
     widgetIdsToRemove: string[] = [];
     widgetsToUpdate: Map<string, DashboardWidgetModel> = new Map<string, DashboardWidgetModel>();
+    allMeasurements: DataLakeMeasure[];
 
     headerVisible = true;
 
     constructor(private dashboardService: DashboardService,
+                private datalakeRestService: DatalakeRestService,
                 private dialogService: DialogService,
                 private dialog: MatDialog,
                 private refreshDashboardService: RefreshDashboardService,
@@ -72,6 +80,7 @@ export class DashboardPanelComponent implements OnInit {
         });
 
         this.getDashboard(params.id);
+        this.getAllMeasurements();
 
     }
 
@@ -84,6 +93,9 @@ export class DashboardPanelComponent implements OnInit {
         });
     }
 
+    getAllMeasurements(): void {
+        this.datalakeRestService.getAllMeasurementSeries().subscribe(res => this.allMeasurements = res);
+    }
 
     addWidget(): void {
         const dialogRef = this.dialogService.open(AddVisualizationDialogComponent, {
diff --git a/ui/src/app/dashboard/components/widget/dashboard-widget.component.html b/ui/src/app/dashboard/components/widget/dashboard-widget.component.html
index 282e5df45..d7a4ec7d6 100644
--- a/ui/src/app/dashboard/components/widget/dashboard-widget.component.html
+++ b/ui/src/app/dashboard/components/widget/dashboard-widget.component.html
@@ -51,113 +51,127 @@
             </div>
             <div *ngIf="widgetLoaded && pipelineRunning" class="h-100">
                 <div *ngIf="configuredWidget.widgetType === 'number'" class="h-100 p-0">
-                    <number-widget [itemWidth]="itemWidth"
+                    <number-widget [itemWidth]="itemWidth" #activeWidget
                                    [itemHeight]="itemHeight"
                                    [editMode]="editMode"
+                                   [globalRefresh]="globalRefresh"
                                    [gridsterItemComponent]="gridsterItemComponent"
                                    [widgetConfig]="configuredWidget" [widgetDataConfig]="widgetDataConfig"
                                    class="h-100"></number-widget>
                 </div>
                 <div *ngIf="configuredWidget.widgetType === 'line'" class="h-100 p-0">
-                    <line-widget [itemWidth]="itemWidth"
+                    <line-widget [itemWidth]="itemWidth" #activeWidget
                                  [itemHeight]="itemHeight"
                                  [editMode]="editMode"
+                                 [globalRefresh]="globalRefresh"
                                  [gridsterItemComponent]="gridsterItemComponent"
                                  [widgetConfig]="configuredWidget"
                                  [widgetDataConfig]="widgetDataConfig" class="h-100"></line-widget>
                 </div>
                 <div *ngIf="configuredWidget.widgetType === 'area'" class="h-100 p-0">
-                    <area-widget [itemWidth]="itemWidth"
+                    <area-widget [itemWidth]="itemWidth" #activeWidget
                                  [itemHeight]="itemHeight"
                                  [editMode]="editMode"
+                                 [globalRefresh]="globalRefresh"
                                  [gridsterItemComponent]="gridsterItemComponent"
                                  [widgetConfig]="configuredWidget"
                                  [widgetDataConfig]="widgetDataConfig" class="h-100"></area-widget>
                 </div>
                 <div *ngIf="configuredWidget.widgetType === 'table'" class="h-100 p-0">
-                    <table-widget [itemWidth]="itemWidth"
+                    <table-widget [itemWidth]="itemWidth" #activeWidget
                                   [itemHeight]="itemHeight"
                                   [editMode]="editMode"
+                                  [globalRefresh]="globalRefresh"
                                   [gridsterItemComponent]="gridsterItemComponent"
                                   [widgetConfig]="configuredWidget"
                                   [widgetDataConfig]="widgetDataConfig" class="h-100"></table-widget>
                 </div>
                 <div *ngIf="configuredWidget.widgetType === 'gauge'" class="h-100 p-0">
-                    <gauge-widget [itemWidth]="itemWidth"
+                    <gauge-widget [itemWidth]="itemWidth" #activeWidget
                                   [itemHeight]="itemHeight"
                                   [editMode]="editMode"
+                                  [globalRefresh]="globalRefresh"
                                   [gridsterItemComponent]="gridsterItemComponent"
                                   [widgetConfig]="configuredWidget"
                                   [widgetDataConfig]="widgetDataConfig" class="h-100"></gauge-widget>
                 </div>
                 <div *ngIf="configuredWidget.widgetType === 'image'" class="h-100 p-0">
-                    <image-widget [itemWidth]="itemWidth"
+                    <image-widget [itemWidth]="itemWidth" #activeWidget
                                   [itemHeight]="itemHeight"
                                   [editMode]="editMode"
+                                  [globalRefresh]="globalRefresh"
                                   [gridsterItemComponent]="gridsterItemComponent"
                                   [widgetConfig]="configuredWidget"
                                   [widgetDataConfig]="widgetDataConfig" class="h-100"></image-widget>
                 </div>
                 <div *ngIf="configuredWidget.widgetType === 'map'" class="h-100 p-0">
-                    <map-widget [itemWidth]="itemWidth"
+                    <map-widget [itemWidth]="itemWidth" #activeWidget
                                 [itemHeight]="itemHeight"
                                 [editMode]="editMode"
+                                [globalRefresh]="globalRefresh"
                                 [gridsterItemComponent]="gridsterItemComponent"
                                 [widgetConfig]="configuredWidget"
                                 [widgetDataConfig]="widgetDataConfig" class="h-100"></map-widget>
                 </div>
                 <div *ngIf="configuredWidget.widgetType === 'raw'" class="h-100 p-0">
-                    <raw-widget [itemWidth]="itemWidth"
+                    <raw-widget [itemWidth]="itemWidth" #activeWidget
                                 [itemHeight]="itemHeight"
                                 [editMode]="editMode"
+                                [globalRefresh]="globalRefresh"
                                 [gridsterItemComponent]="gridsterItemComponent"
                                 [widgetConfig]="configuredWidget"
                                 [widgetDataConfig]="widgetDataConfig" class="h-100"></raw-widget>
                 </div>
                 <div *ngIf="configuredWidget.widgetType === 'html'" class="h-100 p-0">
-                    <html-widget [itemWidth]="itemWidth"
+                    <html-widget [itemWidth]="itemWidth" #activeWidget
                                  [itemHeight]="itemHeight"
                                  [editMode]="editMode"
+                                 [globalRefresh]="globalRefresh"
                                  [gridsterItemComponent]="gridsterItemComponent"
                                  [widgetConfig]="configuredWidget"
                                  [widgetDataConfig]="widgetDataConfig" class="h-100"></html-widget>
                 </div>
                 <div *ngIf="configuredWidget.widgetType === 'trafficlight'" class="h-100 p-0">
-                    <traffic-light-widget [itemWidth]="itemWidth"
+                    <traffic-light-widget [itemWidth]="itemWidth" #activeWidget
                                           [itemHeight]="itemHeight"
                                           [editMode]="editMode"
+                                          [globalRefresh]="globalRefresh"
                                           [gridsterItemComponent]="gridsterItemComponent"
                                           [widgetConfig]="configuredWidget"
                                           [widgetDataConfig]="widgetDataConfig" class="h-100"></traffic-light-widget>
                 </div>
                 <div *ngIf="configuredWidget.widgetType === 'wordcloud'" class="h-100 p-0">
-                    <wordcloud-widget [itemWidth]="itemWidth"
+                    <wordcloud-widget [itemWidth]="itemWidth" #activeWidget
                                       [itemHeight]="itemHeight"
                                       [editMode]="editMode"
+                                      [globalRefresh]="globalRefresh"
                                       [gridsterItemComponent]="gridsterItemComponent"
                                       [widgetConfig]="configuredWidget"
                                       [widgetDataConfig]="widgetDataConfig" class="h-100"></wordcloud-widget>
                 </div>
                 <div *ngIf="configuredWidget.widgetType === 'status'" class="h-100 p-0">
-                    <status-widget [itemWidth]="itemWidth"
+                    <status-widget [itemWidth]="itemWidth" #activeWidget
                                    [itemHeight]="itemHeight"
                                    [editMode]="editMode"
+                                   [globalRefresh]="globalRefresh"
                                    [gridsterItemComponent]="gridsterItemComponent"
                                    [widgetConfig]="configuredWidget"
                                    [widgetDataConfig]="widgetDataConfig" class="h-100"></status-widget>
                 </div>
                 <div *ngIf="configuredWidget.widgetType === 'bar-race'" class="h-100 p-0">
-                    <bar-race-widget [itemWidth]="itemWidth"
+                    <bar-race-widget [itemWidth]="itemWidth" #activeWidget
                                      [itemHeight]="itemHeight"
                                      [editMode]="editMode"
+                                     [globalRefresh]="globalRefresh"
                                      [gridsterItemComponent]="gridsterItemComponent"
                                      [widgetConfig]="configuredWidget"
                                      [widgetDataConfig]="widgetDataConfig" class="h-100"></bar-race-widget>
                 </div>
                 <div *ngIf="configuredWidget.widgetType === 'stacked-line-chart'" class="h-100 p-0">
-                    <stacked-line-chart-widget [itemWidth]="itemWidth"
+                    <stacked-line-chart-widget [itemWidth]="itemWidth" #activeWidget
                                                [itemHeight]="itemHeight"
                                                [editMode]="editMode"
+                                               [globalRefresh]="globalRefresh"
                                                [gridsterItemComponent]="gridsterItemComponent"
                                                [widgetConfig]="configuredWidget"
                                                [widgetDataConfig]="widgetDataConfig"
diff --git a/ui/src/app/dashboard/components/widget/dashboard-widget.component.ts b/ui/src/app/dashboard/components/widget/dashboard-widget.component.ts
index b35f46003..85b12ec7b 100644
--- a/ui/src/app/dashboard/components/widget/dashboard-widget.component.ts
+++ b/ui/src/app/dashboard/components/widget/dashboard-widget.component.ts
@@ -16,17 +16,26 @@
  *
  */
 
-import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import {
+  Component,
+  EventEmitter,
+  Input,
+  OnInit,
+  Output,
+  QueryList, ViewChild,
+  ViewChildren,
+  ViewContainerRef
+} from '@angular/core';
 import { AddVisualizationDialogComponent } from '../../dialogs/add-widget/add-visualization-dialog.component';
 import {
   DashboardItem,
   DashboardService,
   DashboardWidgetModel,
-  DataLakeMeasure,
+  DataLakeMeasure, DatalakeQueryParameters,
   DatalakeRestService,
   DataViewDataExplorerService,
   Pipeline,
-  PipelineService
+  PipelineService, SpQueryResult
 } from '@streampipes/platform-services';
 import { DialogService, PanelType } from '@streampipes/shared-ui';
 import { EditModeService } from '../../services/edit-mode.service';
@@ -35,6 +44,8 @@ import { zip } from 'rxjs';
 import { GridsterItemComponent } from 'angular-gridster2';
 import { ResizeService } from '../../services/resize.service';
 import { GridsterInfo } from '../../models/gridster-info.model';
+import { BaseDataExplorerWidgetDirective } from '../../../data-explorer/components/widgets/base/base-data-explorer-widget.directive';
+import { BaseStreamPipesWidget } from '../widgets/base/base-widget';
 
 @Component({
   selector: 'dashboard-widget',
@@ -49,6 +60,8 @@ export class DashboardWidgetComponent implements OnInit {
   @Input() itemWidth: number;
   @Input() itemHeight: number;
   @Input() gridsterItemComponent: GridsterItemComponent;
+  @Input() globalRefresh: boolean;
+  @Input() allMeasurements: DataLakeMeasure[];
 
   @Output() deleteCallback: EventEmitter<DashboardItem> = new EventEmitter<DashboardItem>();
   @Output() updateCallback: EventEmitter<DashboardWidgetModel> = new EventEmitter<DashboardWidgetModel>();
@@ -61,6 +74,8 @@ export class DashboardWidgetComponent implements OnInit {
   pipelineRunning = false;
   widgetNotAvailable = false;
 
+  _activeWidget: BaseStreamPipesWidget;
+
   constructor(private dashboardService: DashboardService,
               private dialogService: DialogService,
               private pipelineService: PipelineService,
@@ -86,10 +101,10 @@ export class DashboardWidgetComponent implements OnInit {
   }
 
   loadVisualizablePipeline() {
-    zip(this.dataExplorerService.getPersistedDataStream(this.configuredWidget.pipelineId, this.configuredWidget.visualizationName), this.dataLakeRestService.getAllMeasurementSeries())
+    zip(this.dataExplorerService.getPersistedDataStream(this.configuredWidget.pipelineId, this.configuredWidget.visualizationName))
       .subscribe(res => {
         const vizPipeline = res[0];
-        const measurement = res[1].find(m => m.measureName === vizPipeline.measureName);
+        const measurement = this.allMeasurements.find(m => m.measureName === vizPipeline.measureName);
         vizPipeline.eventSchema = measurement.eventSchema;
         this.widgetDataConfig = vizPipeline;
         this.dashboardService.getPipelineById(vizPipeline.pipelineId).subscribe(pipeline => {
@@ -151,4 +166,25 @@ export class DashboardWidgetComponent implements OnInit {
       }
     });
   }
+
+  @ViewChild('activeWidget')
+  set activeWidget(activeWidget: BaseStreamPipesWidget) {
+    this._activeWidget = activeWidget;
+  }
+
+  getWidgetQuery(): DatalakeQueryParameters {
+    if (this._activeWidget) {
+      return this._activeWidget.buildQuery(true);
+    } else {
+      return undefined;
+    }
+  }
+
+  processQueryResponse(res: SpQueryResult) {
+    this._activeWidget.processQueryResult(res);
+  }
+
+  getWidgetId(): string {
+    return this.widget.id;
+  }
 }
diff --git a/ui/src/app/dashboard/components/widgets/base/base-widget.ts b/ui/src/app/dashboard/components/widgets/base/base-widget.ts
index 4a202c2b0..9a801a474 100644
--- a/ui/src/app/dashboard/components/widgets/base/base-widget.ts
+++ b/ui/src/app/dashboard/components/widgets/base/base-widget.ts
@@ -24,7 +24,7 @@ import { ResizeService } from '../../../services/resize.service';
 import {
   DashboardWidgetModel,
   DataLakeMeasure,
-  DatalakeQueryParameterBuilder,
+  DatalakeQueryParameterBuilder, DatalakeQueryParameters,
   DatalakeRestService, EventPropertyPrimitive, EventPropertyUnion, FieldConfig,
   SpQueryResult
 } from '@streampipes/platform-services';
@@ -50,6 +50,7 @@ export abstract class BaseStreamPipesWidget implements OnInit, OnChanges, OnDest
   @Input() itemWidth: number;
   @Input() itemHeight: number;
   @Input() editMode: boolean;
+  @Input() globalRefresh: boolean;
 
   subscription: Subscription;
   intervalSubject: BehaviorSubject<number>;
@@ -83,15 +84,17 @@ export abstract class BaseStreamPipesWidget implements OnInit, OnChanges, OnDest
 
     this.prepareConfigExtraction();
 
-    this.fireQuery().subscribe(result => this.processQueryResult(result));
+    if (!(this.globalRefresh)) {
+      this.fireQuery().subscribe(result => this.processQueryResult(result));
 
-    this.intervalSubject = new BehaviorSubject<number>(this.refreshIntervalInSeconds);
-    this.subscription = this.intervalSubject.pipe(
-      switchMap(val => interval(val * 1000)))
-      .pipe(exhaustMap(() => this.fireQuery()))
-      .subscribe((result) => {
-        this.processQueryResult(result);
-      });
+      this.intervalSubject = new BehaviorSubject<number>(this.refreshIntervalInSeconds);
+      this.subscription = this.intervalSubject.pipe(
+        switchMap(val => interval(val * 1000)))
+        .pipe(exhaustMap(() => this.fireQuery()))
+        .subscribe((result) => {
+          this.processQueryResult(result);
+        });
+    }
   }
 
   prepareConfigExtraction() {
@@ -121,9 +124,13 @@ export abstract class BaseStreamPipesWidget implements OnInit, OnChanges, OnDest
   }
 
   ngOnDestroy(): void {
-    this.subscription.unsubscribe();
-    this.intervalSubject.unsubscribe();
-    this.resizeSub.unsubscribe();
+    if (this.subscription) {
+      this.subscription.unsubscribe();
+      this.intervalSubject.unsubscribe();
+    }
+    if (this.resizeSub) {
+      this.resizeSub.unsubscribe();
+    }
   }
 
   computeCurrentWidth(width: number): number {
@@ -183,7 +190,7 @@ export abstract class BaseStreamPipesWidget implements OnInit, OnChanges, OnDest
     }
   }
 
-  buildQuery() {
+  public buildQuery(includeMeasure = false): DatalakeQueryParameters {
     const queryBuilder = DatalakeQueryParameterBuilder.create();
     const columns = this.getFieldsToQuery();
     if (columns) {
@@ -197,10 +204,17 @@ export abstract class BaseStreamPipesWidget implements OnInit, OnChanges, OnDest
       queryBuilder.withColumnFilter(fields, false);
     }
 
-    return queryBuilder
+    const queryParams = queryBuilder
       .withLimit(this.queryLimit)
       .withOrdering('DESC')
       .build();
+
+    if (includeMeasure) {
+      queryParams.measureName = this.widgetDataConfig.measureName;
+      queryParams.forId = this.widgetConfig._id;
+    }
+
+    return queryParams;
   }
 
   getAnyMeasurementField(eventProperties: EventPropertyUnion[]): string {
diff --git a/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.html b/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.html
index a653ab005..d45e60a30 100644
--- a/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.html
+++ b/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.html
@@ -33,7 +33,18 @@
                 </mat-form-field>
                 <mat-checkbox [(ngModel)]="dashboard.displayHeader">Show name and description in dashboard
                 </mat-checkbox>
+                <div fxLayout="column" class="mt-10">
+                    <mat-checkbox [(ngModel)]="dashboard.dashboardGeneralSettings.globalRefresh">
+                        Use same update frequency for all widgets (will override widget-specific intervals)
+                    </mat-checkbox>
 
+                    <mat-form-field class="full-width mt-10"
+                                    color="accent"
+                                    *ngIf="dashboard.dashboardGeneralSettings.globalRefresh">
+                        <mat-label>Refresh interval (seconds)</mat-label>
+                        <input matInput [(ngModel)]="dashboard.dashboardGeneralSettings.refreshIntervalInSeconds">
+                    </mat-form-field>
+                </div>
             </div>
         </div>
     </div>