/// /// Copyright © 2016-2025 The Thingsboard Authors /// /// Licensed 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, Inject, InjectionToken, OnDestroy, OnInit, Optional, Output, ViewContainerRef } from '@angular/core'; import { AggregationType, clearTimewindowConfig, currentHistoryTimewindow, currentRealtimeTimewindow, historyAllowedAggIntervals, HistoryWindowType, historyWindowTypeTranslations, Interval, MINUTE, QuickTimeInterval, realtimeAllowedAggIntervals, RealtimeWindowType, realtimeWindowTypeTranslations, Timewindow, TimewindowAdvancedParams, TimewindowType, updateFormValuesOnTimewindowTypeChange } from '@shared/models/time/time.models'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { TimeService } from '@core/services/time.service'; import { deepClone, isDefined } from '@core/utils'; import { OverlayRef } from '@angular/cdk/overlay'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { TranslateService } from '@ngx-translate/core'; import { MatDialog } from '@angular/material/dialog'; import { TimewindowConfigDialogComponent, TimewindowConfigDialogData } from '@shared/components/time/timewindow-config-dialog.component'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; export interface TimewindowPanelData { historyOnly: boolean; forAllTimeEnabled: boolean; quickIntervalOnly: boolean; timewindow: Timewindow; aggregation: boolean; timezone: boolean; isEdit: boolean; panelMode: boolean; } export const TIMEWINDOW_PANEL_DATA = new InjectionToken('TimewindowPanelData'); @Component({ selector: 'tb-timewindow-panel', templateUrl: './timewindow-panel.component.html', styleUrls: ['./timewindow-panel.component.scss', './timewindow-form.scss'] }) export class TimewindowPanelComponent extends PageComponent implements OnInit, OnDestroy { @Output() changeTimewindow = new EventEmitter(); historyOnly = false; forAllTimeEnabled = false; quickIntervalOnly = false; aggregation = false; timezone = false; isEdit = false; panelMode = true; timewindow: Timewindow; timewindowForm: UntypedFormGroup; historyTypes = HistoryWindowType; realtimeTypes = RealtimeWindowType; timewindowTypes = TimewindowType; aggregationTypes = AggregationType; result: Timewindow; timewindowTypeOptions: ToggleHeaderOption[] = [{ name: this.translate.instant('timewindow.history'), value: this.timewindowTypes.HISTORY }]; realtimeTimewindowOptions: ToggleHeaderOption[] = []; historyTimewindowOptions: ToggleHeaderOption[] = []; realtimeTypeSelectionAvailable: boolean; realtimeIntervalSelectionAvailable: boolean; historyTypeSelectionAvailable: boolean; historyIntervalSelectionAvailable: boolean; aggregationOptionsAvailable: boolean; realtimeDisableCustomInterval: boolean; realtimeDisableCustomGroupInterval: boolean; historyDisableCustomInterval: boolean; historyDisableCustomGroupInterval: boolean; realtimeAdvancedParams: TimewindowAdvancedParams; realtimeAllowedLastIntervals: Array; realtimeAllowedQuickIntervals: Array; historyAdvancedParams: TimewindowAdvancedParams; historyAllowedLastIntervals: Array; historyAllowedQuickIntervals: Array; allowedAggTypes: Array; private destroy$ = new Subject(); constructor(@Inject(TIMEWINDOW_PANEL_DATA) public data: TimewindowPanelData, @Optional() public overlayRef: OverlayRef, protected store: Store, public fb: UntypedFormBuilder, private timeService: TimeService, private translate: TranslateService, public viewContainerRef: ViewContainerRef, private dialog: MatDialog) { super(store); this.historyOnly = data.historyOnly; this.forAllTimeEnabled = data.forAllTimeEnabled; this.quickIntervalOnly = data.quickIntervalOnly; this.timewindow = data.timewindow; this.aggregation = data.aggregation; this.timezone = data.timezone; this.isEdit = data.isEdit; this.panelMode = data.panelMode; this.updateTimewindowAdvancedParams(); if (!this.historyOnly) { this.timewindowTypeOptions.unshift({ name: this.translate.instant('timewindow.realtime'), value: this.timewindowTypes.REALTIME }); } if ((this.isEdit || !this.timewindow.realtime?.hideLastInterval) && !this.quickIntervalOnly) { this.realtimeTimewindowOptions.push({ name: this.translate.instant(realtimeWindowTypeTranslations.get(RealtimeWindowType.LAST_INTERVAL)), value: this.realtimeTypes.LAST_INTERVAL }); } if (this.isEdit || !this.timewindow.realtime?.hideQuickInterval || this.quickIntervalOnly) { this.realtimeTimewindowOptions.push({ name: this.translate.instant(realtimeWindowTypeTranslations.get(RealtimeWindowType.INTERVAL)), value: this.realtimeTypes.INTERVAL }); } if (this.forAllTimeEnabled) { this.historyTimewindowOptions.push({ name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.FOR_ALL_TIME)), value: this.historyTypes.FOR_ALL_TIME }); } if (this.isEdit || !this.timewindow.history?.hideLastInterval) { this.historyTimewindowOptions.push({ name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.LAST_INTERVAL)), value: this.historyTypes.LAST_INTERVAL }); } if (this.isEdit || !this.timewindow.history?.hideFixedInterval) { this.historyTimewindowOptions.push({ name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.FIXED)), value: this.historyTypes.FIXED }); } if (this.isEdit || !this.timewindow.history?.hideQuickInterval) { this.historyTimewindowOptions.push({ name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.INTERVAL)), value: this.historyTypes.INTERVAL }); } this.realtimeTypeSelectionAvailable = this.realtimeTimewindowOptions.length > 1; this.historyTypeSelectionAvailable = this.historyTimewindowOptions.length > 1; this.realtimeIntervalSelectionAvailable = this.isEdit || !(this.timewindow.realtime?.hideInterval || (this.timewindow.realtime?.hideLastInterval && this.timewindow.realtime?.hideQuickInterval)); this.historyIntervalSelectionAvailable = this.isEdit || !(this.timewindow.history?.hideInterval || (this.timewindow.history?.hideLastInterval && this.timewindow.history?.hideQuickInterval && this.timewindow.history?.hideFixedInterval)); this.aggregationOptionsAvailable = this.aggregation && (this.isEdit || !(this.timewindow.hideAggregation && this.timewindow.hideAggInterval)); } ngOnInit(): void { const hideAggregation = this.timewindow.hideAggregation || false; const hideAggInterval = this.timewindow.hideAggInterval || false; const hideTimezone = this.timewindow.hideTimezone || false; const realtime = this.timewindow.realtime; const history = this.timewindow.history; const aggregation = this.timewindow.aggregation; if (!this.isEdit) { if (realtime?.hideLastInterval && !realtime?.hideQuickInterval) { realtime.realtimeType = RealtimeWindowType.INTERVAL; } if (realtime?.hideQuickInterval && !realtime?.hideLastInterval) { realtime.realtimeType = RealtimeWindowType.LAST_INTERVAL; } if (history?.hideLastInterval) { if (!history.hideFixedInterval) { history.historyType = HistoryWindowType.FIXED; } else if (!history.hideQuickInterval) { history.historyType = HistoryWindowType.INTERVAL; } } if (history?.hideFixedInterval) { if (!history.hideLastInterval) { history.historyType = HistoryWindowType.LAST_INTERVAL; } else if (!history.hideQuickInterval) { history.historyType = HistoryWindowType.INTERVAL; } } if (history?.hideQuickInterval) { if (!history.hideLastInterval) { history.historyType = HistoryWindowType.LAST_INTERVAL; } else if (!history.hideFixedInterval) { history.historyType = HistoryWindowType.FIXED; } } } this.timewindowForm = this.fb.group({ selectedTab: [isDefined(this.timewindow.selectedTab) ? this.timewindow.selectedTab : TimewindowType.REALTIME], realtime: this.fb.group({ realtimeType: [{ value: isDefined(realtime?.realtimeType) ? realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL, disabled: realtime?.hideInterval }], timewindowMs: [{ value: isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : MINUTE, disabled: realtime?.hideInterval || realtime?.hideLastInterval }], interval: [{ value:isDefined(realtime?.interval) ? realtime.interval : null, disabled: hideAggInterval }], quickInterval: [{ value: isDefined(realtime?.quickInterval) ? realtime.quickInterval : QuickTimeInterval.CURRENT_DAY, disabled: realtime?.hideInterval || realtime?.hideQuickInterval }] }), history: this.fb.group({ historyType: [{ value: isDefined(history?.historyType) ? history.historyType : HistoryWindowType.LAST_INTERVAL, disabled: history?.hideInterval }], timewindowMs: [{ value: isDefined(history?.timewindowMs) ? history.timewindowMs : MINUTE, disabled: history?.hideInterval || history?.hideLastInterval }], interval: [{ value:isDefined(history?.interval) ? history.interval : null, disabled: hideAggInterval }], fixedTimewindow: [{ value: isDefined(history?.fixedTimewindow) && this.timewindow.selectedTab === TimewindowType.HISTORY && history.historyType === HistoryWindowType.FIXED ? history.fixedTimewindow : null, disabled: history?.hideInterval || history?.hideFixedInterval }], quickInterval: [{ value: isDefined(history?.quickInterval) ? history.quickInterval : QuickTimeInterval.CURRENT_DAY, disabled: history?.hideInterval || history?.hideQuickInterval }] }), aggregation: this.fb.group({ type: [{ value: isDefined(aggregation?.type) ? aggregation.type : null, disabled: hideAggregation }], limit: [{ value: isDefined(aggregation?.limit) ? aggregation.limit : null, disabled: hideAggInterval }, []] }), timezone: [{ value: isDefined(this.timewindow.timezone) ? this.timewindow.timezone : null, disabled: hideTimezone }] }); this.updateValidators(this.timewindowForm.get('aggregation.type').value); if (this.aggregation) { this.timewindowForm.get('realtime.timewindowMs').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((timewindowMs: number) => { if (this.realtimeAdvancedParams?.lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) && this.realtimeAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval) { setTimeout(() => this.timewindowForm.get('realtime.interval').patchValue( this.realtimeAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false} )); } }); this.timewindowForm.get('realtime.quickInterval').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((quickInterval: number) => { if (this.realtimeAdvancedParams?.quickAggIntervalsConfig?.hasOwnProperty(quickInterval) && this.realtimeAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval) { setTimeout(() => this.timewindowForm.get('realtime.interval').patchValue( this.realtimeAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval, {emitEvent: false} )); } }); this.timewindowForm.get('history.timewindowMs').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((timewindowMs: number) => { if (this.historyAdvancedParams?.lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) && this.historyAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval) { setTimeout(() => this.timewindowForm.get('history.interval').patchValue( this.historyAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false} )); } }); this.timewindowForm.get('history.quickInterval').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((quickInterval: number) => { if (this.historyAdvancedParams?.quickAggIntervalsConfig?.hasOwnProperty(quickInterval) && this.historyAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval) { setTimeout(() => this.timewindowForm.get('history.interval').patchValue( this.historyAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval, {emitEvent: false} )); } }); this.timewindowForm.get('aggregation.type').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((aggregationType: AggregationType) => { this.updateValidators(aggregationType); }); } this.timewindowForm.get('selectedTab').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((selectedTab: TimewindowType) => { this.onTimewindowTypeChange(selectedTab); }); if (!this.panelMode) { this.timewindowForm.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(() => { this.prepareTimewindowConfig(); this.changeTimewindow.emit(this.clearTimewindowConfig()); }); } } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } private updateValidators(aggType: AggregationType) { if (aggType !== AggregationType.NONE) { this.timewindowForm.get('aggregation.limit').clearValidators(); } else { this.timewindowForm.get('aggregation.limit').setValidators([Validators.required]); } this.timewindowForm.get('aggregation.limit').updateValueAndValidity({emitEvent: false}); } private onTimewindowTypeChange(selectedTab: TimewindowType) { updateFormValuesOnTimewindowTypeChange(selectedTab, this.quickIntervalOnly, this.timewindowForm, this.realtimeDisableCustomInterval, this.historyDisableCustomInterval, this.realtimeAdvancedParams, this.historyAdvancedParams); } update() { this.prepareTimewindowConfig(); this.result = this.clearTimewindowConfig(); this.overlayRef?.dispose(); } private prepareTimewindowConfig() { const timewindowFormValue = this.timewindowForm.getRawValue(); this.timewindow.selectedTab = timewindowFormValue.selectedTab; if (this.timewindow.selectedTab === TimewindowType.REALTIME) { this.timewindow.realtime = {...this.timewindow.realtime, ...{ realtimeType: timewindowFormValue.realtime.realtimeType, timewindowMs: timewindowFormValue.realtime.timewindowMs, quickInterval: timewindowFormValue.realtime.quickInterval, }}; } else { this.timewindow.history = {...this.timewindow.history, ...{ historyType: timewindowFormValue.history.historyType, timewindowMs: timewindowFormValue.history.timewindowMs, fixedTimewindow: timewindowFormValue.history.fixedTimewindow, quickInterval: timewindowFormValue.history.quickInterval, }}; } if (this.aggregation) { this.timewindow.realtime.interval = timewindowFormValue.realtime.interval; this.timewindow.history.interval = timewindowFormValue.history.interval; this.timewindow.aggregation = { type: timewindowFormValue.aggregation.type, limit: timewindowFormValue.aggregation.limit }; } if (this.timezone) { this.timewindow.timezone = timewindowFormValue.timezone; } } private clearTimewindowConfig(): Timewindow { return clearTimewindowConfig(this.timewindow, this.quickIntervalOnly, this.historyOnly, this.aggregation, this.timezone); } private updateTimewindowForm() { this.timewindowForm.patchValue(this.timewindow, {emitEvent: false}); this.updateValidators(this.timewindowForm.get('aggregation.type').value); if (this.timewindow.realtime.hideInterval) { this.timewindowForm.get('realtime.realtimeType').disable({emitEvent: false}); this.timewindowForm.get('realtime.timewindowMs').disable({emitEvent: false}); this.timewindowForm.get('realtime.quickInterval').disable({emitEvent: false}); } else { this.timewindowForm.get('realtime.realtimeType').enable({emitEvent: false}); if (this.timewindow.realtime.hideLastInterval) { this.timewindowForm.get('realtime.timewindowMs').disable({emitEvent: false}); } else { this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false}); } if (this.timewindow.realtime.hideQuickInterval) { this.timewindowForm.get('realtime.quickInterval').disable({emitEvent: false}); } else { this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false}); } } if (this.timewindow.history.hideInterval) { this.timewindowForm.get('history.historyType').disable({emitEvent: false}); this.timewindowForm.get('history.timewindowMs').disable({emitEvent: false}); this.timewindowForm.get('history.fixedTimewindow').disable({emitEvent: false}); this.timewindowForm.get('history.quickInterval').disable({emitEvent: false}); } else { this.timewindowForm.get('history.historyType').enable({emitEvent: false}); if (this.timewindow.history.hideLastInterval) { this.timewindowForm.get('history.timewindowMs').disable({emitEvent: false}); } else { this.timewindowForm.get('history.timewindowMs').enable({emitEvent: false}); } if (this.timewindow.history.hideFixedInterval) { this.timewindowForm.get('history.fixedTimewindow').disable({emitEvent: false}); } else { this.timewindowForm.get('history.fixedTimewindow').enable({emitEvent: false}); } if (this.timewindow.history.hideQuickInterval) { this.timewindowForm.get('history.quickInterval').disable({emitEvent: false}); } else { this.timewindowForm.get('history.quickInterval').enable({emitEvent: false}); } } if (this.timewindow.hideAggregation) { this.timewindowForm.get('aggregation.type').disable({emitEvent: false}); } else { this.timewindowForm.get('aggregation.type').enable({emitEvent: false}); } if (this.timewindow.hideAggInterval) { this.timewindowForm.get('aggregation.limit').disable({emitEvent: false}); this.timewindowForm.get('realtime.interval').disable({emitEvent: false}); this.timewindowForm.get('history.interval').disable({emitEvent: false}); } else { this.timewindowForm.get('aggregation.limit').enable({emitEvent: false}); this.timewindowForm.get('realtime.interval').enable({emitEvent: false}); this.timewindowForm.get('history.interval').enable({emitEvent: false}); } if (this.timewindow.hideTimezone) { this.timewindowForm.get('timezone').disable({emitEvent: false}); } else { this.timewindowForm.get('timezone').enable({emitEvent: false}); } this.timewindowForm.markAsDirty(); } cancel() { this.overlayRef?.dispose(); } get minRealtimeAggInterval() { return this.timeService.minIntervalLimit(this.currentRealtimeTimewindow()); } get maxRealtimeAggInterval() { return this.timeService.maxIntervalLimit(this.currentRealtimeTimewindow()); } private currentRealtimeTimewindow(): number { return currentRealtimeTimewindow(this.timewindowForm.getRawValue()); } get minHistoryAggInterval() { return this.timeService.minIntervalLimit(this.currentHistoryTimewindow()); } get maxHistoryAggInterval() { return this.timeService.maxIntervalLimit(this.currentHistoryTimewindow()); } private currentHistoryTimewindow(): number { return currentHistoryTimewindow(this.timewindowForm.getRawValue()); } get realtimeAllowedAggIntervals(): Array { return realtimeAllowedAggIntervals(this.timewindowForm.getRawValue(), this.realtimeAdvancedParams); } get historyAllowedAggIntervals(): Array { return historyAllowedAggIntervals(this.timewindowForm.getRawValue(), this.historyAdvancedParams); } openTimewindowConfig() { this.prepareTimewindowConfig(); this.dialog.open( TimewindowConfigDialogComponent, { autoFocus: false, disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { quickIntervalOnly: this.quickIntervalOnly, aggregation: this.aggregation, timewindow: deepClone(this.timewindow) } }).afterClosed() .subscribe((res) => { if (res) { this.timewindow = res; this.updateTimewindowAdvancedParams(); this.updateTimewindowForm(); } }); } private updateTimewindowAdvancedParams() { this.realtimeDisableCustomInterval = this.timewindow.realtime?.disableCustomInterval; this.realtimeDisableCustomGroupInterval = this.timewindow.realtime?.disableCustomGroupInterval; this.historyDisableCustomInterval = this.timewindow.history?.disableCustomInterval; this.historyDisableCustomGroupInterval = this.timewindow.history?.disableCustomGroupInterval; if (this.timewindow.realtime?.advancedParams) { this.realtimeAdvancedParams = this.timewindow.realtime.advancedParams; this.realtimeAllowedLastIntervals = this.timewindow.realtime.advancedParams.allowedLastIntervals; this.realtimeAllowedQuickIntervals = this.timewindow.realtime.advancedParams.allowedQuickIntervals; } else { this.realtimeAdvancedParams = null; this.realtimeAllowedLastIntervals = null; this.realtimeAllowedQuickIntervals = null; } if (this.timewindow.history?.advancedParams) { this.historyAdvancedParams = this.timewindow.history.advancedParams; this.historyAllowedLastIntervals = this.timewindow.history.advancedParams.allowedLastIntervals; this.historyAllowedQuickIntervals = this.timewindow.history.advancedParams.allowedQuickIntervals; } else { this.historyAdvancedParams = null; this.historyAllowedLastIntervals = null; this.historyAllowedQuickIntervals = null; } this.allowedAggTypes = this.timewindow.allowedAggTypes; } }