feat: Add time selection menu for single widgets in dashboard (#3240)

diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/dashboard/DashboardItem.java b/streampipes-model/src/main/java/org/apache/streampipes/model/dashboard/DashboardItem.java
index c7962b0..8175964 100644
--- a/streampipes-model/src/main/java/org/apache/streampipes/model/dashboard/DashboardItem.java
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/dashboard/DashboardItem.java
@@ -18,6 +18,7 @@
 package org.apache.streampipes.model.dashboard;
 
 import java.util.List;
+import java.util.Map;
 
 public class DashboardItem {
 
@@ -31,6 +32,7 @@
   private Integer rows;
   private Integer x;
   private Integer y;
+  private Map<String, Object> timeSettings;
 
   public DashboardItem() {
 
@@ -99,4 +101,12 @@
   public void setY(Integer y) {
     this.y = y;
   }
+
+  public Map<String, Object> getTimeSettings() {
+    return timeSettings;
+  }
+
+  public void setTimeSettings(Map<String, Object> timeSettings) {
+    this.timeSettings = timeSettings;
+  }
 }
diff --git a/ui/projects/streampipes/platform-services/src/lib/model/dashboard/dashboard.model.ts b/ui/projects/streampipes/platform-services/src/lib/model/dashboard/dashboard.model.ts
index 8a65f93..532a19b 100644
--- a/ui/projects/streampipes/platform-services/src/lib/model/dashboard/dashboard.model.ts
+++ b/ui/projects/streampipes/platform-services/src/lib/model/dashboard/dashboard.model.ts
@@ -25,6 +25,7 @@
 export interface ClientDashboardItem extends GridsterItem {
     widgetId: string;
     widgetType: string;
+    timeSettings?: TimeSettings;
     id: string;
 }
 
diff --git a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DateRange.ts b/ui/projects/streampipes/platform-services/src/lib/model/datalake/DateRange.ts
index 6c305f6..9f3cff7 100644
--- a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DateRange.ts
+++ b/ui/projects/streampipes/platform-services/src/lib/model/datalake/DateRange.ts
@@ -24,6 +24,11 @@
     timeSelectionId?: TimeSelectionId;
 }
 
+export interface WidgetTimeSettings {
+    timeSettings: TimeSettings;
+    widgetIndex?: number;
+}
+
 export interface TimeString {
     startDate: string;
     startTime: 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 7c9650e..398a67c 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
@@ -20,7 +20,7 @@
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2024-09-10 17:11:03.
+// Generated using typescript-generator version 3.2.1263 on 2024-09-18 15:50:05.
 
 export class NamedStreamPipesEntity implements Storable {
     '@class':
@@ -970,6 +970,7 @@
     name: string;
     rows: number;
     settings: string[];
+    timeSettings: { [index: string]: any };
     x: number;
     y: number;
 
@@ -989,6 +990,9 @@
         instance.settings = __getCopyArrayFn(__identity<string>())(
             data.settings,
         );
+        instance.timeSettings = __getCopyObjectFn(__identity<any>())(
+            data.timeSettings,
+        );
         instance.x = data.x;
         instance.y = data.y;
         return instance;
diff --git a/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.html b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.html
index 9e277f0..1564d9b 100644
--- a/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.html
+++ b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.html
@@ -74,6 +74,7 @@
         </div>
     </div>
     <div fxLayout="row" fxLayoutAlign="end center" class="mt-10">
+        <ng-content> </ng-content>
         <button
             mat-raised-button
             color="accent"
diff --git a/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.html b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.html
index 70ded63..e53550a 100644
--- a/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.html
+++ b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.html
@@ -46,6 +46,7 @@
                     (timeSettingsEmitter)="timeSettingsEmitter.emit($event)"
                     class="w-100"
                 >
+                    <ng-content> </ng-content>
                 </sp-custom-time-range-selection>
             </div>
         </div>
diff --git a/ui/src/app/data-explorer/components/widget/data-explorer-dashboard-widget.component.html b/ui/src/app/data-explorer/components/widget/data-explorer-dashboard-widget.component.html
index fa9617a..ea476a9 100644
--- a/ui/src/app/data-explorer/components/widget/data-explorer-dashboard-widget.component.html
+++ b/ui/src/app/data-explorer/components/widget/data-explorer-dashboard-widget.component.html
@@ -113,6 +113,34 @@
                 </button>
                 <button
                     mat-icon-button
+                    [matMenuTriggerFor]="optMenu"
+                    *ngIf="!globalTimeEnabled"
+                    aria-label="Options"
+                    data-cy="options-data-explorer"
+                    #menuTrigger="matMenuTrigger"
+                >
+                    <mat-icon
+                        [color]="timeSettingsModified ? 'primary' : 'default'"
+                        >alarm_clock</mat-icon
+                    >
+                </button>
+                <mat-menu #optMenu="matMenu" class="large-menu">
+                    <sp-time-selector-menu
+                        [timeSettings]="clonedTimeSettings"
+                        (timeSettingsEmitter)="modifyWidgetTimeSettings($event)"
+                        class="w-100"
+                    >
+                        <button
+                            mat-raised-button
+                            class="mat-basic"
+                            (click)="resetWidgetTimeSettings()"
+                        >
+                            Reset
+                        </button>
+                    </sp-time-selector-menu>
+                </mat-menu>
+                <button
+                    mat-icon-button
                     (click)="removeWidget()"
                     matTooltip="Delete widget"
                     *ngIf="editMode && hasDataExplorerWritePrivileges"
diff --git a/ui/src/app/data-explorer/components/widget/data-explorer-dashboard-widget.component.ts b/ui/src/app/data-explorer/components/widget/data-explorer-dashboard-widget.component.ts
index 9e3e962..21e5170 100644
--- a/ui/src/app/data-explorer/components/widget/data-explorer-dashboard-widget.component.ts
+++ b/ui/src/app/data-explorer/components/widget/data-explorer-dashboard-widget.component.ts
@@ -22,9 +22,11 @@
     ComponentRef,
     EventEmitter,
     Input,
+    OnChanges,
     OnDestroy,
     OnInit,
     Output,
+    SimpleChanges,
     ViewChild,
 } from '@angular/core';
 import { GridsterItemComponent } from 'angular-gridster2';
@@ -45,13 +47,18 @@
 import { CurrentUserService } from '@streampipes/shared-ui';
 import { BaseWidgetData } from '../../models/dataview-dashboard.model';
 import { DataExplorerDashboardService } from '../../services/data-explorer-dashboard.service';
+import { TimeSelectionService } from '../../services/time-selection.service';
+import { MatMenuTrigger } from '@angular/material/menu';
 
 @Component({
     selector: 'sp-data-explorer-dashboard-widget',
     templateUrl: './data-explorer-dashboard-widget.component.html',
     styleUrls: ['./data-explorer-dashboard-widget.component.scss'],
 })
-export class DataExplorerDashboardWidgetComponent implements OnInit, OnDestroy {
+export class DataExplorerDashboardWidgetComponent
+    implements OnInit, OnDestroy, OnChanges
+{
+    @ViewChild('menuTrigger') menu: MatMenuTrigger;
     @Input()
     dashboardItem: DashboardItem;
 
@@ -97,6 +104,9 @@
     timerActive = false;
     loadingTime = 0;
 
+    clonedTimeSettings: TimeSettings;
+    timeSettingsModified: boolean = false;
+
     hasDataExplorerWritePrivileges = false;
 
     authSubscription: Subscription;
@@ -116,8 +126,16 @@
         private widgetTypeService: WidgetTypeService,
         private authService: AuthService,
         private currentUserService: CurrentUserService,
+        private timeSelectionService: TimeSelectionService,
     ) {}
 
+    ngOnChanges(changes: SimpleChanges): void {
+        if (changes.widgetIndex && this.componentRef?.instance) {
+            this.componentRef.instance.widgetIndex =
+                changes.widgetIndex.currentValue;
+        }
+    }
+
     ngOnInit(): void {
         this.authSubscription = this.currentUserService.user$.subscribe(
             user => {
@@ -139,6 +157,14 @@
                 },
             );
         this.chooseWidget(this.configuredWidget.widgetType);
+        this.clonedTimeSettings = {
+            startTime: this.timeSettings.startTime,
+            endTime: this.timeSettings.endTime,
+            timeSelectionId: this.timeSettings.timeSelectionId,
+        };
+        if (this.dashboardItem.timeSettings !== undefined) {
+            this.timeSettingsModified = true;
+        }
     }
 
     ngOnDestroy() {
@@ -176,6 +202,7 @@
         this.componentRef.instance.dataExplorerWidget = this.configuredWidget;
         this.componentRef.instance.previewMode = this.previewMode;
         this.componentRef.instance.gridMode = this.gridMode;
+        this.componentRef.instance.widgetIndex = this.widgetIndex;
         const removeSub =
             this.componentRef.instance.removeWidgetCallback.subscribe(ev =>
                 this.removeWidget(),
@@ -196,9 +223,13 @@
     }
 
     getTimeSettings(): TimeSettings {
-        return this.globalTimeEnabled
-            ? this.timeSettings
-            : (this.configuredWidget.timeSettings as TimeSettings);
+        if (this.globalTimeEnabled) {
+            return this.timeSettings;
+        } else if (this.dashboardItem.timeSettings !== undefined) {
+            return this.dashboardItem.timeSettings as TimeSettings;
+        } else {
+            return this.configuredWidget.timeSettings as TimeSettings;
+        }
     }
 
     removeWidget() {
@@ -233,4 +264,26 @@
             this.configuredWidget,
         );
     }
+
+    modifyWidgetTimeSettings(timeSettings: TimeSettings): void {
+        this.dashboardItem.timeSettings = timeSettings;
+        this.timeSelectionService.notify(timeSettings, this.widgetIndex);
+        this.menu.closeMenu();
+        this.timeSettingsModified = true;
+    }
+
+    resetWidgetTimeSettings(): void {
+        this.dashboardItem.timeSettings = undefined;
+        this.clonedTimeSettings = {
+            startTime: this.timeSettings.startTime,
+            endTime: this.timeSettings.endTime,
+            timeSelectionId: this.timeSettings.timeSelectionId,
+        };
+        this.timeSelectionService.notify(
+            this.getTimeSettings(),
+            this.widgetIndex,
+        );
+        this.menu.closeMenu();
+        this.timeSettingsModified = false;
+    }
 }
diff --git a/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-widget.directive.ts b/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-widget.directive.ts
index b8e86ff..5df627e 100644
--- a/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-widget.directive.ts
+++ b/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-widget.directive.ts
@@ -83,6 +83,9 @@
     @Input() dataViewDashboardItem: DashboardItem;
     @Input() dataExplorerWidget: T;
 
+    @Input()
+    widgetIndex: number;
+
     @HostBinding('class') className = 'h-100';
 
     public selectedProperties: string[];
@@ -193,16 +196,21 @@
         }
         this.timeSelectionSub =
             this.timeSelectionService.timeSelectionChangeSubject.subscribe(
-                ts => {
-                    if (ts) {
-                        this.timeSettings = ts;
-                    } else {
-                        this.timeSelectionService.updateTimeSettings(
-                            this.timeSettings,
-                            new Date(),
-                        );
+                widgetTimeSettings => {
+                    if (
+                        widgetTimeSettings.widgetIndex === undefined ||
+                        widgetTimeSettings.widgetIndex === this.widgetIndex
+                    ) {
+                        if (widgetTimeSettings.timeSettings) {
+                            this.timeSettings = widgetTimeSettings.timeSettings;
+                        } else {
+                            this.timeSelectionService.updateTimeSettings(
+                                this.timeSettings,
+                                new Date(),
+                            );
+                        }
+                        this.updateData();
                     }
-                    this.updateData();
                 },
             );
         this.updateData();
diff --git a/ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-dashboard-dialog.component.html b/ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-dashboard-dialog.component.html
index de73cca..98b4312 100644
--- a/ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-dashboard-dialog.component.html
+++ b/ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-dashboard-dialog.component.html
@@ -67,7 +67,8 @@
                         [(ngModel)]="
                             dashboard.dashboardGeneralSettings.globalTimeEnabled
                         "
-                        >Use global time instead of widget time settings
+                        >Use global time settings instead of widget time
+                        settings
                     </mat-checkbox>
                 </div>
                 <!--<mat-checkbox [(ngModel)]="dashboard.displayHeader">Show name and description in dashboard</mat-checkbox>-->
diff --git a/ui/src/app/data-explorer/models/dataview-dashboard.model.ts b/ui/src/app/data-explorer/models/dataview-dashboard.model.ts
index 670c73a..d3597f6 100644
--- a/ui/src/app/data-explorer/models/dataview-dashboard.model.ts
+++ b/ui/src/app/data-explorer/models/dataview-dashboard.model.ts
@@ -54,6 +54,7 @@
     dataExplorerWidget: T;
     previewMode: boolean;
     gridMode: boolean;
+    widgetIndex?: number;
 
     cleanupSubscriptions(): void;
 }
diff --git a/ui/src/app/data-explorer/services/time-selection.service.ts b/ui/src/app/data-explorer/services/time-selection.service.ts
index 11a9515..5c55e77 100644
--- a/ui/src/app/data-explorer/services/time-selection.service.ts
+++ b/ui/src/app/data-explorer/services/time-selection.service.ts
@@ -23,6 +23,7 @@
     QuickTimeSelection,
     TimeSelectionId,
     TimeSettings,
+    WidgetTimeSettings,
 } from '@streampipes/platform-services';
 import {
     startOfDay,
@@ -178,10 +179,11 @@
         }
     }
 
-    public timeSelectionChangeSubject: Subject<TimeSettings | undefined> =
-        new Subject<TimeSettings | undefined>();
+    public timeSelectionChangeSubject: Subject<WidgetTimeSettings | undefined> =
+        new Subject<WidgetTimeSettings | undefined>();
 
-    public notify(timeSettings?: TimeSettings): void {
-        this.timeSelectionChangeSubject.next(timeSettings);
+    public notify(timeSettings?: TimeSettings, widgetIndex?: number): void {
+        const widgetTimeSettings = { timeSettings, widgetIndex };
+        this.timeSelectionChangeSubject.next(widgetTimeSettings);
     }
 }