diff --git a/ui/cypress/support/utils/datalake/DataLakeUtils.ts b/ui/cypress/support/utils/datalake/DataLakeUtils.ts index dd52f048e4..0dee52c86d 100644 --- a/ui/cypress/support/utils/datalake/DataLakeUtils.ts +++ b/ui/cypress/support/utils/datalake/DataLakeUtils.ts @@ -24,6 +24,7 @@ import { FileManagementUtils } from '../FileManagementUtils'; import { ConnectUtils } from '../connect/ConnectUtils'; import { ConnectBtns } from '../connect/ConnectBtns'; import { AdapterBuilder } from '../../builder/AdapterBuilder'; +import { differenceInMonths } from 'date-fns'; export class DataLakeUtils { public static goToDatalake() { @@ -349,8 +350,41 @@ export class DataLakeUtils { } public static selectTimeRange(from: Date, to: Date) { - DataLakeUtils.setTimeInput('time-range-from', from); - DataLakeUtils.setTimeInput('time-range-to', to); + DataLakeUtils.openTimeSelectorMenu(); + const monthsBack = Math.abs(differenceInMonths(from, new Date())); + DataLakeUtils.navigateCalendar('previous', monthsBack); + DataLakeUtils.selectDay(from.getDay()); + + const monthsForward = Math.abs(differenceInMonths(from, to)); + DataLakeUtils.navigateCalendar('next', monthsForward); + + DataLakeUtils.selectDay(to.getDay()); + + DataLakeUtils.setTimeInput('time-selector-start-time', from); + DataLakeUtils.setTimeInput('time-selector-end-time', to); + DataLakeUtils.applyCustomTimeSelection(); + } + + public static navigateCalendar(direction: string, numberOfMonths: number) { + for (let i = 0; i < numberOfMonths; i++) { + cy.get(`button.mat-calendar-${direction}-button`).click(); + } + } + + public static selectDay(day: number) { + cy.get( + `button:has(span.mat-calendar-body-cell-content:contains("${day}"))`, + ) + .first() + .click(); + } + + public static openTimeSelectorMenu() { + cy.dataCy('time-selector-menu').click(); + } + + public static applyCustomTimeSelection() { + cy.dataCy('apply-custom-time').click(); } public static setTimeInput(field: string, date: Date) { @@ -358,7 +392,7 @@ export class DataLakeUtils { } public static makeTimeString(date: Date) { - return date.toISOString().slice(0, 16); + return date.toTimeString().slice(0, 5); } public static getFutureDate() { diff --git a/ui/cypress/tests/datalake/timeRangeSelectors.spec.ts b/ui/cypress/tests/datalake/timeRangeSelectors.spec.ts index fd60b73af7..a8626a8374 100644 --- a/ui/cypress/tests/datalake/timeRangeSelectors.spec.ts +++ b/ui/cypress/tests/datalake/timeRangeSelectors.spec.ts @@ -17,20 +17,29 @@ */ import { DataLakeUtils } from '../../support/utils/datalake/DataLakeUtils'; +import { + subDays, + subHours, + subMinutes, + subMonths, + subWeeks, + subYears, +} from 'date-fns'; describe('Test Time Range Selectors in Data Explorer', () => { const periods = [ - { selector: '15_min', offset: 15 }, - { selector: '1_hour', offset: 60 }, - { selector: '1_day', offset: 1440 }, - { selector: '1_week', offset: 10080 }, - { selector: '1_month', offset: 43200 }, - { selector: '1_year', offset: 525600 }, + { selector: 1, start: (now: Date) => subMinutes(now, 15) }, + { selector: 2, start: (now: Date) => subHours(now, 1) }, + { selector: 4, start: (now: Date) => subDays(now, 1) }, + { selector: 6, start: (now: Date) => subWeeks(now, 1) }, + { selector: 8, start: (now: Date) => subMonths(now, 1) }, + { selector: 10, start: (now: Date) => subYears(now, 1) }, ]; - const dateAttribute = 'ng-reflect-date'; - const timeRangeFrom = 'time-range-from'; - const timeRangeTo = 'time-range-to'; + const timeRangeFrom = 'time-selector-start-time'; + const timeRangeTo = 'time-selector-end-time'; + const dateRangeFrom = 'time-selector-start-date'; + const dateRangeTo = 'time-selector-end-date'; before('Setup Tests', () => { cy.initStreamPipesTest(); @@ -42,74 +51,59 @@ describe('Test Time Range Selectors in Data Explorer', () => { it('Perform Test', () => { periods.forEach(period => { cy.log('Testing period: ' + period.selector); + DataLakeUtils.openTimeSelectorMenu(); // Choosing time period and saving initial start and end dates - cy.dataCy(period.selector).click(); - cy.dataCy(timeRangeTo) - .invoke('attr', dateAttribute) - .then(initialEndDateString => { - cy.dataCy(timeRangeFrom) - .invoke('attr', dateAttribute) - .then(initialStartDateString => { - const initialStartDate = parseDate( - initialStartDateString, - ); - const initialEndDate = - parseDate(initialEndDateString); - - cy.wrap(initialStartDate).should( - 'deep.eq', - getExpectedStartDate( - initialEndDate, - period.offset, - ), - ); - // Updating time range to previous one and checking start and end dates - cy.dataCy('decrease-time-button').click({ - force: true, - }); - cy.dataCy(timeRangeTo) - .invoke('attr', dateAttribute) - .then(updatedEndDateString => { - cy.dataCy(timeRangeFrom) - .invoke('attr', dateAttribute) - .then(updatedStartDateString => { - const updatedStartDate = parseDate( - updatedStartDateString, - ); - const updatedEndDate = - parseDate(updatedEndDateString); + cy.dataCy(`time-selector-quick-${period.selector}`).click(); + const expectedEndDate = new Date(); + DataLakeUtils.openTimeSelectorMenu(); + // check if dates can differ from the selected dates + const expectedStartDate = getExpectedStartDate( + expectedEndDate, + period.start, + ); + cy.dataCy(dateRangeFrom).should( + 'have.text', + getLocalizedDateString(expectedStartDate), + ); + cy.dataCy(dateRangeTo).should( + 'have.text', + getLocalizedDateString(expectedEndDate), + ); - cy.wrap(updatedStartDate).should( - 'deep.eq', - getExpectedStartDate( - updatedEndDate, - period.offset, - ), - ); - cy.wrap(updatedEndDate).should( - 'deep.eq', - initialStartDate, - ); - }); - }); - // Updating time range to the next one and comparing start and end dates with initial values - cy.dataCy('increase-time-button').click({ - force: true, - }); - cy.dataCy(timeRangeFrom) - .invoke('attr', dateAttribute) - .should('eq', initialStartDateString); - cy.dataCy(timeRangeTo) - .invoke('attr', dateAttribute) - .should('eq', initialEndDateString); - }); + cy.dataCy(timeRangeFrom) + .invoke('val') + .then(actualTime => { + const expectedDate = + getLocalizedTimeString(expectedStartDate); + expect( + isTimeWithinTolerance( + actualTime as string, + expectedDate, + 3, + ), + ).to.be.true; + }); + cy.dataCy(timeRangeTo) + .invoke('val') + .then(actualTime => { + const expectedDate = + getLocalizedTimeString(expectedEndDate); + expect( + isTimeWithinTolerance( + actualTime as string, + expectedDate, + 3, + ), + ).to.be.true; }); + + DataLakeUtils.applyCustomTimeSelection(); }); }); }); -function getExpectedStartDate(endDate: Date, offset: number): Date { - const startDate = new Date(endDate.getTime() - offset * 60000); +function getExpectedStartDate(endDate: Date, startFn: (Date) => Date): Date { + const startDate = startFn(endDate); startDate.setMinutes( startDate.getMinutes() + getTimezoneDifference(endDate, startDate), ); @@ -120,6 +114,28 @@ function getTimezoneDifference(endDate: Date, startDate: Date): number { return endDate.getTimezoneOffset() - startDate.getTimezoneOffset(); } -function parseDate(dateString: string): Date { - return new Date(Date.parse(dateString)); +function getLocalizedDateString(date: Date) { + return date.toLocaleDateString(); +} + +function getLocalizedTimeString(date: Date) { + return date.toLocaleTimeString().slice(0, 8); +} + +function parseTimeStringToSeconds(timeString: string) { + const [hours, minutes, seconds] = timeString.split(':').map(Number); + return hours * 3600 + minutes * 60 + seconds || 0; +} + +function isTimeWithinTolerance( + actualTimeString: string, + expectedTimeString: string, + toleranceInSeconds: number, +) { + const actualTimeInSeconds = parseTimeStringToSeconds(actualTimeString); + const expectedTimeInSeconds = parseTimeStringToSeconds(expectedTimeString); + return ( + Math.abs(actualTimeInSeconds - expectedTimeInSeconds) <= + toleranceInSeconds + ); } diff --git a/ui/projects/streampipes/platform-services/src/lib/apis/data-view-data-explorer.service.ts b/ui/projects/streampipes/platform-services/src/lib/apis/data-view-data-explorer.service.ts index 68c1988cb4..af101d07a0 100644 --- a/ui/projects/streampipes/platform-services/src/lib/apis/data-view-data-explorer.service.ts +++ b/ui/projects/streampipes/platform-services/src/lib/apis/data-view-data-explorer.service.ts @@ -40,6 +40,10 @@ export class DataViewDataExplorerService { return this.sharedDatalakeRestService.getDashboards(this.dashboardUrl); } + getDashboard(dashboardId: string): Observable { + return this.http.get(`${this.dashboardUrl}/${dashboardId}`); + } + updateDashboard(dashboard: Dashboard): Observable { return this.sharedDatalakeRestService.updateDashboard( this.dashboardUrl, 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 87151909ca..6c305f6dd4 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 @@ -19,7 +19,39 @@ export interface TimeSettings { startTime: number; endTime: number; - dynamicSelection: 15 | 60 | 1440 | 10080 | 43800 | 525600 | -1; + // deprecated + dynamicSelection?: 15 | 60 | 1440 | 10080 | 43800 | 525600 | -1; + timeSelectionId?: TimeSelectionId; +} + +export interface TimeString { + startDate: string; + startTime: string; + endDate: string; + endTime: string; +} + +export enum TimeSelectionId { + CUSTOM, + LAST_15_MINUTES, + LAST_HOUR, + CURRENT_HOUR, + LAST_DAY, + CURRENT_DAY, + LAST_WEEK, + CURRENT_WEEK, + LAST_MONTH, + CURRENT_MONTH, + LAST_YEAR, + CURRENT_YEAR, +} + +export interface QuickTimeSelection { + label: string; + timeSelectionId: TimeSelectionId; + startTime: (now: Date) => Date; + endTime: (now: Date) => Date; + addDividerAfter?: boolean; } export class DateRange { diff --git a/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts b/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts index 2ac32f916b..2c2c28c4f7 100644 --- a/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts +++ b/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts @@ -51,6 +51,7 @@ export class DataViewQueryGeneratorService { return this.dataLakeRestService.getData( sourceConfig.measureName, dataLakeConfiguration, + true, ); }); } diff --git a/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/dashboard-toolbar.component.html b/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/dashboard-toolbar.component.html index ff1b2ee7b6..1309cd85b7 100644 --- a/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/dashboard-toolbar.component.html +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/dashboard-toolbar.component.html @@ -78,9 +78,9 @@ fxLayout="row" > diff --git a/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/dashboard-toolbar.component.ts b/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/dashboard-toolbar.component.ts index e2da2c238d..e0e64fa740 100644 --- a/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/dashboard-toolbar.component.ts +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/dashboard-toolbar.component.ts @@ -43,6 +43,9 @@ export class DataExplorerDashboardToolbarComponent { @Input() timeSettings: TimeSettings; + @Input() + globalTimeEnabled = true; + @Output() viewModeChange: EventEmitter = new EventEmitter(); diff --git a/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-preview/data-view-preview.component.html b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-preview/data-view-preview.component.html index dc77d00e81..e6727f9e1b 100644 --- a/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-preview/data-view-preview.component.html +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-preview/data-view-preview.component.html @@ -21,7 +21,7 @@
{{ dataView.baseAppearanceConfig.widgetTitle }}
insert_chartinsert_chart {{ widgetTypeLabel @@ -29,7 +29,7 @@
{{ dataView.baseAppearanceConfig.widgetTitle }}
folderfolder {{ dataView.dataConfig.sourceConfigs[0].measureName diff --git a/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.html b/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.html index f5e4d0e2be..bf55d8dbcc 100644 --- a/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.html +++ b/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.html @@ -23,6 +23,9 @@ >
This data view is empty and doesn't contain any widgets. -
[timeSettings]="timeSettings" (deleteCallback)="removeDataViewFromDashboard($event)" (startEditModeEmitter)="startEditMode($event)" - *ngIf="!showEditingHelpInfo && viewMode === 'slide'" + *ngIf="dashboard.widgets.length > 0 && viewMode === 'slide'" >
diff --git a/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.ts b/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.ts index d29b19a2bd..2f8f78ef6c 100644 --- a/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.ts +++ b/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.ts @@ -25,6 +25,7 @@ import { DataExplorerWidgetModel, DataLakeMeasure, DataViewDataExplorerService, + TimeSelectionId, TimeSettings, } from '@streampipes/platform-services'; import { TimeSelectionService } from '../../services/time-selection.service'; @@ -76,7 +77,6 @@ export class DataExplorerDashboardPanelComponent implements OnInit, OnDestroy { public items: Dashboard[]; dataLakeMeasure: DataLakeMeasure; - showEditingHelpInfo = false; authSubscription: Subscription; constructor( @@ -158,9 +158,13 @@ export class DataExplorerDashboardPanelComponent implements OnInit, OnDestroy { } updateDateRange(timeSettings: TimeSettings) { - this.timeSettings = timeSettings; - this.dashboard.dashboardTimeSettings = timeSettings; - this.timeSelectionService.notify(timeSettings); + let ts = undefined; + if (this.dashboard.dashboardGeneralSettings.globalTimeEnabled) { + this.timeSettings = timeSettings; + this.dashboard.dashboardTimeSettings = timeSettings; + ts = timeSettings; + } + this.timeSelectionService.notify(ts); } discardChanges() { @@ -168,7 +172,6 @@ export class DataExplorerDashboardPanelComponent implements OnInit, OnDestroy { } triggerEditMode() { - this.showEditingHelpInfo = false; this.editMode = true; } @@ -179,10 +182,8 @@ export class DataExplorerDashboardPanelComponent implements OnInit, OnDestroy { } getDashboard(dashboardId: string, startTime: number, endTime: number) { - this.dataViewService.getDataViews().subscribe(data => { - this.dashboard = data.filter( - dashboard => dashboard.elementId === dashboardId, - )[0]; + this.dataViewService.getDashboard(dashboardId).subscribe(dashboard => { + this.dashboard = dashboard; this.breadcrumbService.updateBreadcrumb( this.breadcrumbService.makeRoute( [SpDataExplorerRoutes.BASE], @@ -192,21 +193,37 @@ export class DataExplorerDashboardPanelComponent implements OnInit, OnDestroy { this.viewMode = this.dashboard.dashboardGeneralSettings.defaultViewMode || 'grid'; + if ( + this.dashboard.dashboardGeneralSettings.globalTimeEnabled === + undefined + ) { + this.dashboard.dashboardGeneralSettings.globalTimeEnabled = + true; + } + if (!this.dashboard.dashboardTimeSettings.startTime) { + this.dashboard.dashboardTimeSettings = + this.timeSelectionService.getDefaultTimeSettings(); + } else { + this.timeSelectionService.updateTimeSettings( + this.dashboard.dashboardTimeSettings, + new Date(), + ); + } this.timeSettings = startTime && endTime ? this.overrideTime(+startTime, +endTime) : this.dashboard.dashboardTimeSettings; - if (this.dashboard.widgets.length === 0 && this.editMode) { - this.triggerEditMode(); - } else if (this.dashboard.widgets.length === 0 && !this.editMode) { - this.showEditingHelpInfo = true; - } this.dashboardLoaded = true; }); } overrideTime(startTime: number, endTime: number): TimeSettings { - return { startTime, endTime, dynamicSelection: -1 }; + return { + startTime, + endTime, + dynamicSelection: -1, + timeSelectionId: TimeSelectionId.CUSTOM, + }; } goBackToOverview() { diff --git a/ui/src/app/data-explorer/components/data-view/data-explorer-data-view.component.html b/ui/src/app/data-explorer/components/data-view/data-explorer-data-view.component.html index 5ddeee5a14..ba9a94eaa5 100644 --- a/ui/src/app/data-explorer/components/data-view/data-explorer-data-view.component.html +++ b/ui/src/app/data-explorer/components/data-view/data-explorer-data-view.component.html @@ -30,6 +30,7 @@ >
-
- -
{ - this.dataSettingsPanel.checkSourceTypes(); - }); - } - } - - closeDesignerPanel() { - this.closeDesignerPanelEmitter.emit(); - } - - resetIndex() { - this.selectedIndex = 0; - this.newWidgetMode = true; - } - @ViewChild('dataSettingsPanel') public set content( dataSettingsPanel: DataExplorerWidgetDataSettingsComponent, diff --git a/ui/src/app/data-explorer/components/data-view/data-view-toolbar/data-explorer-data-view-toolbar.component.html b/ui/src/app/data-explorer/components/data-view/data-view-toolbar/data-explorer-data-view-toolbar.component.html index 04ae11e225..3f54d2b57b 100644 --- a/ui/src/app/data-explorer/components/data-view/data-view-toolbar/data-explorer-data-view-toolbar.component.html +++ b/ui/src/app/data-explorer/components/data-view/data-view-toolbar/data-explorer-data-view-toolbar.component.html @@ -65,7 +65,7 @@
diff --git a/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview/data-explorer-dashboard-overview.component.html b/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview/data-explorer-dashboard-overview.component.html index 4ada6cdaed..b4a789b851 100644 --- a/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview/data-explorer-dashboard-overview.component.html +++ b/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview/data-explorer-dashboard-overview.component.html @@ -83,7 +83,7 @@ color="accent" matTooltip="Edit data view settings" *ngIf="hasDataExplorerWritePrivileges" - (click)="openEditDataViewDialog(element)" + (click)="openEditDashboardDialog(element)" > settings diff --git a/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview/data-explorer-dashboard-overview.component.ts b/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview/data-explorer-dashboard-overview.component.ts index 1ccaeb0f4d..4d6b8ca7dd 100644 --- a/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview/data-explorer-dashboard-overview.component.ts +++ b/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview/data-explorer-dashboard-overview.component.ts @@ -82,9 +82,9 @@ export class SpDataExplorerDashboardOverviewComponent extends SpDataExplorerOver }); } - openEditDataViewDialog(dashboard: Dashboard) { + openEditDashboardDialog(dashboard: Dashboard) { const dialogRef = - this.dataExplorerDashboardService.openDataViewModificationDialog( + this.dataExplorerDashboardService.openDashboardModificationDialog( false, dashboard, ); diff --git a/ui/src/app/data-explorer/components/overview/data-explorer-overview.component.ts b/ui/src/app/data-explorer/components/overview/data-explorer-overview.component.ts index 45d057adc4..2d872f7561 100644 --- a/ui/src/app/data-explorer/components/overview/data-explorer-overview.component.ts +++ b/ui/src/app/data-explorer/components/overview/data-explorer-overview.component.ts @@ -62,16 +62,16 @@ export class DataExplorerOverviewComponent extends SpDataExplorerOverviewDirecti dataViewDashboard.widgets = []; dataViewDashboard.name = ''; - this.openDataViewModificationDialog(true, dataViewDashboard); + this.openDashboardModificationDialog(true, dataViewDashboard); } createNewDataView(): void { this.routingService.navigateToDataView(true); } - openDataViewModificationDialog(createMode: boolean, dashboard: Dashboard) { + openDashboardModificationDialog(createMode: boolean, dashboard: Dashboard) { const dialogRef = - this.dataExplorerDashboardService.openDataViewModificationDialog( + this.dataExplorerDashboardService.openDashboardModificationDialog( createMode, dashboard, ); diff --git a/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.html b/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.html new file mode 100644 index 0000000000..3da1b0aafc --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.html @@ -0,0 +1,79 @@ + + +
+
+ + + + + + + + +
+ +
diff --git a/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.scss b/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.scss similarity index 87% rename from ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.scss rename to ui/src/app/data-explorer/components/time-selector/time-range-selector.component.scss index fb24a7dc6f..aef8593a37 100644 --- a/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.scss +++ b/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.scss @@ -55,3 +55,18 @@ .mr--5 { margin-right: -5px; } + +.formatted-date { + font-weight: bolder; +} + +.formatted-time { + font-weight: lighter; +} + +.formatted-datetime { + display: inline; + background: var(--color-bg-2); + border-radius: 5px; + padding: 5px; +} diff --git a/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.ts b/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.ts new file mode 100644 index 0000000000..a2b663d5b3 --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.ts @@ -0,0 +1,146 @@ +/* + * 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, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; +import { + TimeSelectionId, + TimeSettings, + TimeString, +} from '@streampipes/platform-services'; +import { MatMenuTrigger } from '@angular/material/menu'; +import { TimeSelectionService } from '../../services/time-selection.service'; +import { TimeRangeSelectorMenuComponent } from './time-selector-menu/time-selector-menu.component'; + +@Component({ + selector: 'sp-time-range-selector', + templateUrl: 'time-range-selector.component.html', + styleUrls: ['./time-range-selector.component.scss'], + encapsulation: ViewEncapsulation.None, +}) +export class TimeRangeSelectorComponent implements OnInit, OnChanges { + @ViewChild('menuTrigger') menu: MatMenuTrigger; + @ViewChild('timeSelectorMenu') + timeSelectorMenu: TimeRangeSelectorMenuComponent; + + @Output() dateRangeEmitter = new EventEmitter(); + + @Input() + timeSettings: TimeSettings; + + @Input() + showTimeSelector = true; + + simpleTimeString: string = ''; + timeString: TimeString; + timeStringMode: 'simple' | 'advanced' = 'simple'; + + constructor(private timeSelectionService: TimeSelectionService) {} + + ngOnInit() { + this.createDateString(); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.timeSettings) { + this.createDateString(); + } + } + + applyPreviousInterval(): void { + this.changeTimeByInterval((a, b) => a - b); + } + + applyNextInterval(): void { + this.changeTimeByInterval((a, b) => a + b); + } + + compare(newDateRange: TimeSettings, oldDateRange: TimeSettings): boolean { + return ( + newDateRange && + oldDateRange && + newDateRange.startTime === oldDateRange.startTime && + newDateRange.endTime === oldDateRange.endTime && + newDateRange.dynamicSelection === oldDateRange.dynamicSelection + ); + } + + reloadData() { + this.dateRangeEmitter.emit(this.timeSettings); + } + + updateTimeSettingsAndReload() { + this.timeSelectionService.updateTimeSettings( + this.timeSettings, + new Date(), + ); + if (this.showTimeSelector) { + this.timeSelectorMenu.triggerDisplayUpdate(); + } + this.reloadData(); + } + + private changeTimeByInterval(func: (a: number, b: number) => number) { + const difference = + this.timeSettings.endTime - this.timeSettings.startTime; + const newStartTime = func(this.timeSettings.startTime, difference); + const newEndTime = func(this.timeSettings.endTime, difference); + + this.timeSettings.startTime = newStartTime; + this.timeSettings.endTime = newEndTime; + this.timeSettings.timeSelectionId = TimeSelectionId.CUSTOM; + this.timeSelectorMenu.triggerDisplayUpdate(); + this.createDateString(); + this.reloadData(); + } + + applyCurrentDateRange(timeSettings: TimeSettings) { + this.timeSettings = timeSettings; + this.createDateString(); + this.menu.closeMenu(); + this.reloadData(); + } + + createDateString(): void { + if (this.timeSettings.timeSelectionId !== TimeSelectionId.CUSTOM) { + this.simpleTimeString = this.timeSelectionService.getTimeSelection( + this.timeSettings.timeSelectionId, + ).label; + this.timeStringMode = 'simple'; + } else { + const startDate = new Date(this.timeSettings.startTime); + const endDate = new Date(this.timeSettings.endTime); + this.timeString = { + startDate: startDate.toLocaleDateString(), + endDate: endDate.toLocaleDateString(), + startTime: startDate.toLocaleTimeString(), + endTime: endDate.toLocaleTimeString(), + }; + this.timeStringMode = 'advanced'; + } + } +} 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 new file mode 100644 index 0000000000..9e277f0ca6 --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.html @@ -0,0 +1,87 @@ + + +
+
+ + + + + +
+
+
+ {{ currentStartDate }} +
+
+ +
+
+
+
+ {{ currentEndDate }} +
+
+ +
+
+
+
+
+ +
+
diff --git a/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.scss b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.scss new file mode 100644 index 0000000000..ce53cf59ea --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.scss @@ -0,0 +1,22 @@ +/* + * 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. + * + */ + +.time-selector { + font-family: inherit; + font-size: 11pt; +} diff --git a/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.ts b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.ts new file mode 100644 index 0000000000..2129a521c9 --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.ts @@ -0,0 +1,111 @@ +/* + * 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, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { TimeSelectionId, TimeSettings } from '@streampipes/platform-services'; +import { + DateRange, + DefaultMatCalendarRangeStrategy, + MatRangeDateSelectionModel, +} from '@angular/material/datepicker'; + +@Component({ + selector: 'sp-custom-time-range-selection', + templateUrl: 'custom-time-range-selection.component.html', + styleUrls: ['./custom-time-range-selection.component.scss'], +}) +export class CustomTimeRangeSelectionComponent implements OnInit { + @Input() timeSettings: TimeSettings; + @Output() timeSettingsEmitter = new EventEmitter(); + + currentStartDate: string; + currentEndDate: string; + currentStartTime: string; + currentEndTime: string; + currentDateRange: DateRange; + dateSelectionComplete = false; + + constructor( + private readonly selectionModel: MatRangeDateSelectionModel, + private readonly selectionStrategy: DefaultMatCalendarRangeStrategy, + ) {} + + ngOnInit(): void { + this.initializeDateRange(); + this.triggerDisplayUpdate(); + this.dateSelectionComplete = true; + } + + initializeDateRange(): void { + this.currentDateRange = new DateRange( + new Date(this.timeSettings.startTime), + new Date(this.timeSettings.endTime), + ); + } + + triggerDisplayUpdate() { + this.updateDateStrings(); + this.updateTimeStrings(); + } + + updateTimeStrings(): void { + this.currentStartTime = this.formatTime(this.currentDateRange.start); + this.currentEndTime = this.formatTime(this.currentDateRange.end); + } + + formatTime(date: Date): string { + return date.toTimeString().slice(0, 8); + } + + updateDateStrings(): void { + this.currentStartDate = this.formatDate(this.currentDateRange.start); + this.currentEndDate = this.formatDate(this.currentDateRange.end); + } + + formatDate(date: Date): string { + return date?.toLocaleDateString() || '-'; + } + + onDateChange(selectedDate: Date): void { + const newSelection = this.selectionStrategy.selectionFinished( + selectedDate, + this.selectionModel.selection, + ); + this.selectionModel.updateSelection(newSelection, this); + this.currentDateRange = new DateRange( + newSelection.start, + newSelection.end, + ); + this.dateSelectionComplete = this.selectionModel.isComplete(); + this.updateDateStrings(); + } + + saveSelection(): void { + this.updateDateTime(this.currentDateRange.start, this.currentStartTime); + this.updateDateTime(this.currentDateRange.end, this.currentEndTime); + this.timeSettings.startTime = this.currentDateRange.start.getTime(); + this.timeSettings.endTime = this.currentDateRange.end.getTime(); + this.timeSettings.timeSelectionId = TimeSelectionId.CUSTOM; + this.timeSettingsEmitter.emit(this.timeSettings); + } + + updateDateTime(date: Date, time: string): void { + const [hours, minutes, seconds] = time.split(':').map(Number); + date.setHours(hours, minutes, seconds || 0); + } +} 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 new file mode 100644 index 0000000000..70ded63676 --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.html @@ -0,0 +1,53 @@ + + + diff --git a/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.scss b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.scss new file mode 100644 index 0000000000..b9c614c4d3 --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.scss @@ -0,0 +1,44 @@ +/* + * 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. + * + */ + +.menu-content { + padding: 10px; + min-width: 550px; +} + +.filter-header { + font-weight: bold; + white-space: nowrap; + font-size: 12pt; +} + +.filter-section { + padding: 5px 10px 5px 5px; + border: 1px solid var(--color-bg-3); + background: var(--color-bg-1); +} + +.quick-link { + color: var(--color-accent); + cursor: pointer; + font-size: 11pt; +} + +.quick-link:hover { + font-weight: bold; +} diff --git a/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.ts b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.ts new file mode 100644 index 0000000000..a24958bb31 --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.ts @@ -0,0 +1,73 @@ +/* + * 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, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, +} from '@angular/core'; +import { + QuickTimeSelection, + TimeSettings, +} from '@streampipes/platform-services'; +import { TimeSelectionService } from '../../../services/time-selection.service'; +import { CustomTimeRangeSelectionComponent } from './custom-time-range-selection/custom-time-range-selection.component'; + +@Component({ + selector: 'sp-time-selector-menu', + templateUrl: 'time-selector-menu.component.html', + styleUrls: ['./time-selector-menu.component.scss'], +}) +export class TimeRangeSelectorMenuComponent implements OnInit { + @Input() + timeSettings: TimeSettings; + + @Output() + timeSettingsEmitter: EventEmitter = + new EventEmitter(); + + quickSelections: QuickTimeSelection[] = []; + + @ViewChild('timeRangeSelection') + timeRangeSelection: CustomTimeRangeSelectionComponent; + + constructor(private timeSelectionService: TimeSelectionService) {} + + ngOnInit(): void { + this.quickSelections = this.timeSelectionService.quickTimeSelections; + } + + applyQuickSelection(quickSelection: QuickTimeSelection): void { + const selectedDateRange = + this.timeSelectionService.getDateRange(quickSelection); + this.timeSettings.timeSelectionId = quickSelection.timeSelectionId; + this.timeSettings.startTime = selectedDateRange.startDate.getTime(); + this.timeSettings.endTime = selectedDateRange.endDate.getTime(); + this.timeRangeSelection.initializeDateRange(); + this.triggerDisplayUpdate(); + this.timeSettingsEmitter.emit(this.timeSettings); + } + + triggerDisplayUpdate(): void { + this.timeRangeSelection.initializeDateRange(); + this.timeRangeSelection.triggerDisplayUpdate(); + } +} diff --git a/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.html b/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.html deleted file mode 100644 index 29073a02a0..0000000000 --- a/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.html +++ /dev/null @@ -1,82 +0,0 @@ - - -
-
- -
-
- - - - - - - - -
-
- -
-
diff --git a/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.ts b/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.ts deleted file mode 100644 index 6dcea2232c..0000000000 --- a/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* - * 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, - EventEmitter, - Input, - OnInit, - Output, - ViewEncapsulation, -} from '@angular/core'; -import { TimeSettings } from '@streampipes/platform-services'; - -@Component({ - selector: 'sp-time-range-selector', - templateUrl: 'timeRangeSelector.component.html', - styleUrls: ['./timeRangeSelector.component.scss'], - encapsulation: ViewEncapsulation.None, -}) -export class TimeRangeSelectorComponent implements OnInit { - @Output() dateRangeEmitter = new EventEmitter(); - - _dateRange: TimeSettings; - - startDate: Date; - endDate: Date; - - public possibleTimeButtons = [ - { value: '15 min', offset: 15 }, - { value: '1 hour', offset: 60 }, - { value: '1 day', offset: 1440 }, - { value: '1 week', offset: 10080 }, - { value: '1 month', offset: 43200 }, - { value: '1 year', offset: 525600 }, - { value: 'custom', offset: -1 }, - ]; - - public selectedTimeButton; - - constructor() {} - - ngOnInit() { - if (!this.dateRange.startTime) { - this.setCurrentDateRange(this.possibleTimeButtons[0]); - } else if (this.dateRange.dynamicSelection !== -1) { - this.setCurrentDateRange( - this.possibleTimeButtons.find( - tb => tb.offset === this.dateRange.dynamicSelection, - ), - ); - } else { - this.startDate = new Date(this._dateRange.startTime); - this.endDate = new Date(this._dateRange.endTime); - } - } - - @Input() - set dateRange(dateRange: TimeSettings) { - if (!this.compare(dateRange, this._dateRange)) { - this._dateRange = dateRange; - this.updateTimeSettings(); - } - } - - get dateRange(): TimeSettings { - return this._dateRange; - } - - compare(newDateRange: TimeSettings, oldDateRange: TimeSettings): boolean { - return ( - newDateRange && - oldDateRange && - newDateRange.startTime === oldDateRange.startTime && - newDateRange.endTime === oldDateRange.endTime && - newDateRange.dynamicSelection === oldDateRange.dynamicSelection - ); - } - - updateTimeSettings() { - this.startDate = new Date(this.dateRange.startTime); - this.endDate = new Date(this.dateRange.endTime); - this.selectedTimeButton = this.findOffset( - this.dateRange.dynamicSelection, - ); - this.reloadData(); - } - - findOffset(dynamicSelection: number) { - return ( - this.possibleTimeButtons.find( - el => el.offset === dynamicSelection, - ) || this.possibleTimeButtons[0] - ); - } - - reloadData() { - this.dateRangeEmitter.emit(this.dateRange); - } - - increaseTime() { - this.changeTimeByInterval((a, b) => a + b); - } - - decreaseTime() { - this.changeTimeByInterval((a, b) => a - b); - } - - refreshData() { - const difference = this.endDate.getTime() - this.startDate.getTime(); - - const current = new Date().getTime(); - this.dateRange = { - startTime: current - difference, - endTime: current, - dynamicSelection: this.dateRange.dynamicSelection, - }; - - this.reloadData(); - } - - private changeTimeByInterval(func) { - const difference = this.endDate.getTime() - this.startDate.getTime(); - const newStartTime = func(this.startDate.getTime(), difference); - const newEndTime = func(this.endDate.getTime(), difference); - - this.startDate = new Date(newStartTime); - this.endDate = new Date(newEndTime); - this.selectedTimeButton = - this.possibleTimeButtons[this.possibleTimeButtons.length - 1]; - this.dateRange = { - startTime: newStartTime, - endTime: newEndTime, - dynamicSelection: -1, - }; - } - - changeCustomDateRange() { - this.selectedTimeButton = - this.possibleTimeButtons[this.possibleTimeButtons.length - 1]; - const newStartTime = this.startDate.getTime(); - const newEndTime = this.endDate.getTime(); - - this.dateRange = { - startTime: newStartTime, - endTime: newEndTime, - dynamicSelection: -1, - }; - } - - /** - * Sets the current date range from now to the value of offset in the past - * @param offset in minutes - */ - setCurrentDateRange(item) { - this.selectedTimeButton = item; - const current = new Date().getTime(); - this.startDate = new Date(current - item.offset * 60000); - this.endDate = new Date(current); - this.dateRange = { - startTime: this.startDate.getTime(), - endTime: this.endDate.getTime(), - dynamicSelection: item.offset, - }; - } -} diff --git a/ui/src/app/data-explorer/components/widget-view/abstract-widget-view.directive.ts b/ui/src/app/data-explorer/components/widget-view/abstract-widget-view.directive.ts index 8c1bab95d3..c2a99b1f7b 100644 --- a/ui/src/app/data-explorer/components/widget-view/abstract-widget-view.directive.ts +++ b/ui/src/app/data-explorer/components/widget-view/abstract-widget-view.directive.ts @@ -67,17 +67,6 @@ export abstract class AbstractWidgetViewDirective { protected widgetRegistryService: DataExplorerWidgetRegistry, ) {} - updateAllWidgets() { - this.configuredWidgets.forEach((value, key) => { - this.dataViewDataExplorerService - .updateWidget(value) - .subscribe(response => { - value.rev = response._rev; - this.currentlyConfiguredWidgetId = undefined; - }); - }); - } - startEditMode(value: DataExplorerWidgetModel) { this.startEditModeEmitter.emit(value); this.currentlyConfiguredWidgetId = value.elementId; @@ -143,8 +132,6 @@ export abstract class AbstractWidgetViewDirective { this.deleteCallback.emit(widget); } - propagateItemUpdate(dashboardWidget: DataExplorerWidgetModel) {} - propagateWidgetSelection(configuredWidget: DataExplorerWidgetModel) { if (configuredWidget) { this.currentlyConfiguredWidgetId = configuredWidget.elementId; diff --git a/ui/src/app/data-explorer/components/widget-view/grid-view/data-explorer-dashboard-grid.component.html b/ui/src/app/data-explorer/components/widget-view/grid-view/data-explorer-dashboard-grid.component.html index 1fd05d2a36..86e0f983b0 100644 --- a/ui/src/app/data-explorer/components/widget-view/grid-view/data-explorer-dashboard-grid.component.html +++ b/ui/src/app/data-explorer/components/widget-view/grid-view/data-explorer-dashboard-grid.component.html @@ -30,6 +30,9 @@

{{ dashboard.description }}

= new EventEmitter(); @Output() startEditModeEmitter: EventEmitter = @@ -162,7 +165,7 @@ export class DataExplorerDashboardWidgetComponent implements OnInit, OnDestroy { componentFactory, ); this.componentRef.instance.dataExplorerWidget = this.configuredWidget; - this.componentRef.instance.timeSettings = this.timeSettings; + this.componentRef.instance.timeSettings = this.getTimeSettings(); this.componentRef.instance.gridsterItem = this.dashboardItem; this.componentRef.instance.gridsterItemComponent = this.gridsterItemComponent; @@ -190,6 +193,12 @@ export class DataExplorerDashboardWidgetComponent implements OnInit, OnDestroy { }); } + getTimeSettings(): TimeSettings { + return this.globalTimeEnabled + ? this.timeSettings + : (this.configuredWidget.timeSettings as TimeSettings); + } + removeWidget() { this.deleteCallback.emit(this.configuredWidget); } 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 0bdd480447..02416e6ecd 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 @@ -208,7 +208,14 @@ export abstract class BaseDataExplorerWidgetDirective< this.timeSelectionSub = this.timeSelectionService.timeSelectionChangeSubject.subscribe( ts => { - this.timeSettings = ts; + if (ts) { + this.timeSettings = ts; + } else { + this.timeSelectionService.updateTimeSettings( + this.timeSettings, + new Date(), + ); + } this.updateData(); }, ); @@ -228,10 +235,6 @@ export abstract class BaseDataExplorerWidgetDirective< this.requestQueue$.unsubscribe(); } - public removeWidget() { - this.removeWidgetCallback.emit(true); - } - public setShownComponents( showNoDataInDateRange: boolean, showData: boolean, diff --git a/ui/src/app/data-explorer/components/widgets/table/table-widget.component.ts b/ui/src/app/data-explorer/components/widgets/table/table-widget.component.ts index a156b11b14..40acdf5a48 100644 --- a/ui/src/app/data-explorer/components/widgets/table/table-widget.component.ts +++ b/ui/src/app/data-explorer/components/widgets/table/table-widget.component.ts @@ -51,7 +51,9 @@ export class TableWidgetComponent } regenerateColumnNames(): void { - this.groupByColumnNames = this.makeGroupByColumns(); + this.groupByColumnNames = this.makeGroupByColumns( + this.dataExplorerWidget.visualizationConfig.selectedColumns, + ); this.columnNames = ['time'].concat( this.dataExplorerWidget.visualizationConfig.selectedColumns.map( c => c.fullDbName, @@ -60,10 +62,16 @@ export class TableWidgetComponent ); } - makeGroupByColumns(): string[] { + makeGroupByColumns(selectedColumns: DataExplorerField[]): string[] { return this.dataExplorerWidget.dataConfig.sourceConfigs.flatMap(sc => { return sc.queryConfig.groupBy .filter(g => g.selected) + .filter( + g => + selectedColumns.find( + column => column.runtimeName === g.runtimeName, + ) === undefined, + ) .map(g => g.runtimeName); }); } diff --git a/ui/src/app/data-explorer/data-explorer.module.ts b/ui/src/app/data-explorer/data-explorer.module.ts index 62dc60706a..02cdad7b9b 100644 --- a/ui/src/app/data-explorer/data-explorer.module.ts +++ b/ui/src/app/data-explorer/data-explorer.module.ts @@ -23,7 +23,11 @@ import { FlexLayoutModule } from '@ngbracket/ngx-layout'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatChipsModule } from '@angular/material/chips'; import { MatNativeDateModule } from '@angular/material/core'; -import { MatDatepickerModule } from '@angular/material/datepicker'; +import { + DefaultMatCalendarRangeStrategy, + MatDatepickerModule, + MatRangeDateSelectionModel, +} from '@angular/material/datepicker'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSliderModule } from '@angular/material/slider'; @@ -39,7 +43,7 @@ import { CoreUiModule } from '../core-ui/core-ui.module'; import { DataExplorerDashboardGridComponent } from './components/widget-view/grid-view/data-explorer-dashboard-grid.component'; import { DataExplorerOverviewComponent } from './components/overview/data-explorer-overview.component'; import { DataExplorerDashboardPanelComponent } from './components/dashboard/data-explorer-dashboard-panel.component'; -import { TimeRangeSelectorComponent } from './components/time-selector/timeRangeSelector.component'; +import { TimeRangeSelectorComponent } from './components/time-selector/time-range-selector.component'; import { DataExplorerDashboardWidgetComponent } from './components/widget/data-explorer-dashboard-widget.component'; import { ImageWidgetComponent } from './components/widgets/image/image-widget.component'; import { TableWidgetComponent } from './components/widgets/table/table-widget.component'; @@ -48,7 +52,7 @@ import { LoadDataSpinnerComponent } from './components/widgets/utils/load-data-s import { NoDataInDateRangeComponent } from './components/widgets/utils/no-data/no-data-in-date-range.component'; import { SelectPropertiesComponent } from './components/widgets/utils/select-properties/select-properties.component'; import { SelectColorPropertiesComponent } from './components/widgets/utils/select-color-properties/select-color-properties.component'; -import { DataExplorerEditDataViewDialogComponent } from './dialogs/edit-dashboard/data-explorer-edit-data-view-dialog.component'; +import { DataExplorerEditDashboardDialogComponent } from './dialogs/edit-dashboard/data-explorer-edit-dashboard-dialog.component'; import { GroupConfigurationComponent } from './components/widgets/utils/group-configuration/group-configuration.component'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { DataExplorerDesignerPanelComponent } from './components/data-view/data-view-designer-panel/data-explorer-designer-panel.component'; @@ -121,6 +125,8 @@ import { DataExplorerDataViewSelectionComponent } from './components/dashboard/d import { DataExplorerDashboardWidgetSelectionPanelComponent } from './components/dashboard/dashboard-widget-selection-panel/dashboard-widget-selection-panel.component'; import { DataExplorerDataViewPreviewComponent } from './components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-preview/data-view-preview.component'; import { DataExplorerDashboardToolbarComponent } from './components/dashboard/dashboard-toolbar/dashboard-toolbar.component'; +import { TimeRangeSelectorMenuComponent } from './components/time-selector/time-selector-menu/time-selector-menu.component'; +import { CustomTimeRangeSelectionComponent } from './components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component'; @NgModule({ imports: [ @@ -201,6 +207,7 @@ import { DataExplorerDashboardToolbarComponent } from './components/dashboard/da ], declarations: [ AggregateConfigurationComponent, + CustomTimeRangeSelectionComponent, DataExplorerDashboardGridComponent, DataExplorerOverviewComponent, DataExplorerDashboardPanelComponent, @@ -211,7 +218,7 @@ import { DataExplorerDashboardToolbarComponent } from './components/dashboard/da DataExplorerDataViewPreviewComponent, DataExplorerDataViewSelectionComponent, DataExplorerDesignerPanelComponent, - DataExplorerEditDataViewDialogComponent, + DataExplorerEditDashboardDialogComponent, DataExplorerWidgetAppearanceSettingsComponent, DataExplorerWidgetDataSettingsComponent, DataExplorerDataViewComponent, @@ -239,6 +246,7 @@ import { DataExplorerDashboardToolbarComponent } from './components/dashboard/da HeatmapWidgetConfigComponent, ImageViewerComponent, TimeRangeSelectorComponent, + TimeRangeSelectorMenuComponent, DataExplorerVisualisationSettingsComponent, GroupSelectionPanelComponent, DataExplorerVisualisationSettingsComponent, @@ -260,7 +268,7 @@ import { DataExplorerDashboardToolbarComponent } from './components/dashboard/da SpTimeSeriesAppearanceConfigComponent, SpDataZoomConfigComponent, ], - providers: [], + providers: [DefaultMatCalendarRangeStrategy, MatRangeDateSelectionModel], exports: [], }) export class DataExplorerModule { diff --git a/ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-data-view-dialog.component.html b/ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-dashboard-dialog.component.html similarity index 66% rename from ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-data-view-dialog.component.html rename to ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-dashboard-dialog.component.html index b65bc1e047..de73ccaeff 100644 --- a/ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-data-view-dialog.component.html +++ b/ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-dashboard-dialog.component.html @@ -40,25 +40,36 @@ Description - - - + + - Grid View - - - Slide View - - + + Grid View + + + Slide View + + + +
+ + Use global time instead of widget time settings + +
diff --git a/ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-data-view-dialog.component.scss b/ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-dashboard-dialog.component.scss similarity index 100% rename from ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-data-view-dialog.component.scss rename to ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-dashboard-dialog.component.scss diff --git a/ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-data-view-dialog.component.ts b/ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-dashboard-dialog.component.ts similarity index 77% rename from ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-data-view-dialog.component.ts rename to ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-dashboard-dialog.component.ts index 5fdce655c6..b0e8a66a6d 100644 --- a/ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-data-view-dialog.component.ts +++ b/ui/src/app/data-explorer/dialogs/edit-dashboard/data-explorer-edit-dashboard-dialog.component.ts @@ -18,22 +18,22 @@ import { Component, Input, OnInit } from '@angular/core'; import { - DataViewDataExplorerService, Dashboard, + DataViewDataExplorerService, } from '@streampipes/platform-services'; import { DialogRef } from '@streampipes/shared-ui'; @Component({ - selector: 'sp-data-explorer-edit-data-view-dialog-component', - templateUrl: './data-explorer-edit-data-view-dialog.component.html', - styleUrls: ['./data-explorer-edit-data-view-dialog.component.scss'], + selector: 'sp-data-explorer-edit-dashboard-dialog-component', + templateUrl: './data-explorer-edit-dashboard-dialog.component.html', + styleUrls: ['./data-explorer-edit-dashboard-dialog.component.scss'], }) -export class DataExplorerEditDataViewDialogComponent implements OnInit { +export class DataExplorerEditDashboardDialogComponent implements OnInit { @Input() createMode: boolean; @Input() dashboard: Dashboard; constructor( - private dialogRef: DialogRef, + private dialogRef: DialogRef, private dashboardService: DataViewDataExplorerService, ) {} @@ -41,6 +41,12 @@ export class DataExplorerEditDataViewDialogComponent implements OnInit { if (!this.dashboard.dashboardGeneralSettings.defaultViewMode) { this.dashboard.dashboardGeneralSettings.defaultViewMode = 'grid'; } + if ( + this.dashboard.dashboardGeneralSettings.globalTimeEnabled === + undefined + ) { + this.dashboard.dashboardGeneralSettings.globalTimeEnabled = true; + } } onCancel(): void { diff --git a/ui/src/app/data-explorer/services/data-explorer-dashboard.service.ts b/ui/src/app/data-explorer/services/data-explorer-dashboard.service.ts index e7177e9fa1..dcc5bba228 100644 --- a/ui/src/app/data-explorer/services/data-explorer-dashboard.service.ts +++ b/ui/src/app/data-explorer/services/data-explorer-dashboard.service.ts @@ -24,7 +24,7 @@ import { DateRange, TimeSettings, } from '@streampipes/platform-services'; -import { DataExplorerEditDataViewDialogComponent } from '../dialogs/edit-dashboard/data-explorer-edit-data-view-dialog.component'; +import { DataExplorerEditDashboardDialogComponent } from '../dialogs/edit-dashboard/data-explorer-edit-dashboard-dialog.component'; import { DialogService, PanelType } from '@streampipes/shared-ui'; import { DataDownloadDialogComponent } from '../../core-ui/data-download-dialog/data-download-dialog.component'; @@ -32,13 +32,13 @@ import { DataDownloadDialogComponent } from '../../core-ui/data-download-dialog/ export class DataExplorerDashboardService { constructor(private dialogService: DialogService) {} - openDataViewModificationDialog(createMode: boolean, dashboard: Dashboard) { + openDashboardModificationDialog(createMode: boolean, dashboard: Dashboard) { return this.dialogService.open( - DataExplorerEditDataViewDialogComponent, + DataExplorerEditDashboardDialogComponent, { - panelType: PanelType.STANDARD_PANEL, - title: createMode ? 'New Data View' : 'Edit Data View', - width: '70vw', + panelType: PanelType.SLIDE_IN_PANEL, + title: createMode ? 'New Dashboard' : 'Edit Dashboard', + width: '60vw', data: { createMode: createMode, dashboard: dashboard, 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 81090a04d2..94cdef6361 100644 --- a/ui/src/app/data-explorer/services/time-selection.service.ts +++ b/ui/src/app/data-explorer/services/time-selection.service.ts @@ -18,14 +18,146 @@ import { Injectable } from '@angular/core'; import { Subject } from 'rxjs'; -import { TimeSettings } from '@streampipes/platform-services'; +import { + DateRange, + QuickTimeSelection, + TimeSelectionId, + TimeSettings, +} from '@streampipes/platform-services'; +import { + startOfDay, + startOfHour, + startOfMonth, + startOfWeek, + startOfYear, + subDays, + subHours, + subMinutes, + subMonths, + subWeeks, + subYears, +} from 'date-fns'; @Injectable({ providedIn: 'root' }) export class TimeSelectionService { - public timeSelectionChangeSubject: Subject = - new Subject(); + quickTimeSelections: QuickTimeSelection[] = [ + { + label: 'Last 15 min', + timeSelectionId: TimeSelectionId.LAST_15_MINUTES, + startTime: now => subMinutes(now, 15), + endTime: now => now, + }, + { + label: 'Last 1 hour', + timeSelectionId: TimeSelectionId.LAST_HOUR, + startTime: now => subHours(now, 1), + endTime: now => now, + }, + { + label: 'Last 1 day', + timeSelectionId: TimeSelectionId.LAST_DAY, + startTime: now => subDays(now, 1), + endTime: now => now, + }, + { + label: 'Last 1 week', + timeSelectionId: TimeSelectionId.LAST_WEEK, + startTime: now => subWeeks(now, 1), + endTime: now => now, + }, + { + label: 'Last 1 month', + timeSelectionId: TimeSelectionId.LAST_MONTH, + startTime: now => subMonths(now, 1), + endTime: now => now, + }, + { + label: 'Last 1 year', + timeSelectionId: TimeSelectionId.LAST_YEAR, + startTime: now => subYears(now, 1), + endTime: now => now, + }, + { + label: 'Current day', + timeSelectionId: TimeSelectionId.CURRENT_DAY, + startTime: now => startOfDay(now), + endTime: now => now, + }, + { + label: 'Current hour', + timeSelectionId: TimeSelectionId.CURRENT_HOUR, + startTime: now => startOfHour(now), + endTime: now => now, + }, + { + label: 'Current week', + timeSelectionId: TimeSelectionId.CURRENT_WEEK, + startTime: now => startOfWeek(now), + endTime: now => now, + }, + { + label: 'Current month', + timeSelectionId: TimeSelectionId.CURRENT_MONTH, + startTime: now => startOfMonth(now), + endTime: now => now, + }, + { + label: 'Current year', + timeSelectionId: TimeSelectionId.CURRENT_YEAR, + startTime: now => startOfYear(now), + endTime: now => now, + }, + ]; - public notify(timeSettings: TimeSettings): void { + public getDateRange(quickSelection: QuickTimeSelection): DateRange { + const now = new Date(); + return { + startDate: quickSelection.startTime(now), + endDate: quickSelection.endTime(now), + }; + } + + public getDefaultTimeSettings(): TimeSettings { + return this.getTimeSettings( + TimeSelectionId.LAST_15_MINUTES, + new Date(), + ); + } + + public getTimeSettings( + timeSelectionId: TimeSelectionId, + now: Date, + ): TimeSettings { + const selection = this.getTimeSelection(timeSelectionId); + return { + startTime: selection.startTime(now).getTime(), + endTime: selection.endTime(now).getTime(), + dynamicSelection: -1, + timeSelectionId: timeSelectionId, + }; + } + + public updateTimeSettings(timeSettings: TimeSettings, now: Date): void { + if (timeSettings.timeSelectionId !== TimeSelectionId.CUSTOM) { + const updatedTimeSettings = this.getTimeSettings( + timeSettings.timeSelectionId, + now, + ); + timeSettings.startTime = updatedTimeSettings.startTime; + timeSettings.endTime = updatedTimeSettings.endTime; + } + } + + public getTimeSelection(timeSelectionId: TimeSelectionId) { + return this.quickTimeSelections.find( + s => s.timeSelectionId === timeSelectionId, + ); + } + + public timeSelectionChangeSubject: Subject = + new Subject(); + + public notify(timeSettings?: TimeSettings): void { this.timeSelectionChangeSubject.next(timeSettings); } }