committed by
GitHub
36 changed files with 2349 additions and 664 deletions
@ -0,0 +1,30 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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. |
||||
|
|
||||
|
--> |
||||
|
<div [formGroup]="datapointsLimitFormGroup" class="limit-slider-container" fxLayout="row" fxLayoutAlign="start center"> |
||||
|
<mat-slider fxFlex |
||||
|
min="{{minDatapointsLimit()}}" |
||||
|
max="{{maxDatapointsLimit()}}"> |
||||
|
<input matSliderThumb formControlName="limit" [value]="datapointsLimitFormGroup.get('limit').value"/> |
||||
|
</mat-slider> |
||||
|
<mat-form-field class="limit-slider-value" subscriptSizing="dynamic" appearance="outline"> |
||||
|
<input matInput formControlName="limit" type="number" step="1" |
||||
|
[value]="datapointsLimitFormGroup.get('limit').value" |
||||
|
min="{{minDatapointsLimit()}}" |
||||
|
max="{{maxDatapointsLimit()}}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
@ -0,0 +1,38 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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 "../../../../scss/constants"; |
||||
|
|
||||
|
.limit-slider-container { |
||||
|
width: 100%; |
||||
|
.limit-slider-value { |
||||
|
margin-left: 16px; |
||||
|
min-width: 25px; |
||||
|
max-width: 106px; |
||||
|
} |
||||
|
mat-form-field input[type=number] { |
||||
|
text-align: center; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media #{$mat-gt-sm} { |
||||
|
.limit-slider-container { |
||||
|
> label { |
||||
|
margin-right: 16px; |
||||
|
width: min-content; |
||||
|
max-width: 40%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,160 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
FormGroup, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
||||
|
import { TimeService } from '@core/services/time.service'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-datapoints-limit', |
||||
|
templateUrl: './datapoints-limit.component.html', |
||||
|
styleUrls: ['./datapoints-limit.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => DatapointsLimitComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => DatapointsLimitComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class DatapointsLimitComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { |
||||
|
|
||||
|
datapointsLimitFormGroup: FormGroup; |
||||
|
|
||||
|
modelValue: number | null; |
||||
|
|
||||
|
private requiredValue: boolean; |
||||
|
get required(): boolean { |
||||
|
return this.requiredValue; |
||||
|
} |
||||
|
@Input() |
||||
|
set required(value: boolean) { |
||||
|
const newVal = coerceBooleanProperty(value); |
||||
|
if (this.requiredValue !== newVal) { |
||||
|
this.requiredValue = newVal; |
||||
|
this.updateValidators(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor(private fb: FormBuilder, |
||||
|
private timeService: TimeService) { |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.datapointsLimitFormGroup = this.fb.group({ |
||||
|
limit: [null, [Validators.min(this.minDatapointsLimit()), Validators.max(this.maxDatapointsLimit())]] |
||||
|
}); |
||||
|
this.datapointsLimitFormGroup.get('limit').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((value) => { |
||||
|
this.updateView(value); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
updateValidators() { |
||||
|
if (this.datapointsLimitFormGroup) { |
||||
|
if (this.required) { |
||||
|
this.datapointsLimitFormGroup.get('limit').addValidators(Validators.required); |
||||
|
} else { |
||||
|
this.datapointsLimitFormGroup.get('limit').removeValidators(Validators.required); |
||||
|
} |
||||
|
this.datapointsLimitFormGroup.get('limit').updateValueAndValidity(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.datapointsLimitFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.datapointsLimitFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private checkLimit(limit?: number): number { |
||||
|
if (!limit || limit < this.minDatapointsLimit()) { |
||||
|
return this.minDatapointsLimit(); |
||||
|
} else if (limit > this.maxDatapointsLimit()) { |
||||
|
return this.maxDatapointsLimit(); |
||||
|
} |
||||
|
return limit; |
||||
|
} |
||||
|
|
||||
|
writeValue(value: number | null): void { |
||||
|
this.modelValue = this.checkLimit(value); |
||||
|
this.datapointsLimitFormGroup.patchValue( |
||||
|
{ limit: this.modelValue }, {emitEvent: false} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
updateView(value: number | null) { |
||||
|
if (this.modelValue !== value) { |
||||
|
this.modelValue = value; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors { |
||||
|
return this.datapointsLimitFormGroup.get('limit').valid ? null : { |
||||
|
datapointsLimitFormGroup: false, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
minDatapointsLimit() { |
||||
|
return this.timeService.getMinDatapointsLimit(); |
||||
|
} |
||||
|
|
||||
|
maxDatapointsLimit() { |
||||
|
return this.timeService.getMaxDatapointsLimit(); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,269 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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. |
||||
|
|
||||
|
--> |
||||
|
<form [formGroup]="timewindowForm" mat-dialog-content class="tb-timewindow-form"> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2>{{ 'timewindow.timewindow-settings' | translate }}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<!-- <div tb-help="#"></div>--> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<div class="tb-timewindow-form-header tb-form-panel no-border no-padding-bottom"> |
||||
|
<tb-toggle-select class="tb-timewindow-form-type-options" appearance="fill" |
||||
|
[options]="timewindowTypeOptions" formControlName="selectedTab"> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<div class="tb-timewindow-form-content tb-form-panel no-border"> |
||||
|
<section *ngIf="timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME" |
||||
|
formGroupName="realtime" class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title">{{ 'timewindow.timewindow' | translate }}</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="hideInterval"> |
||||
|
{{ 'timewindow.hide-timewindow-section' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
|
||||
|
<tb-toggle-select *ngIf="realtimeTypeSelectionAvailable" |
||||
|
appearance="stroked" [options]="realtimeTimewindowOptions" formControlName="realtimeType"> |
||||
|
</tb-toggle-select> |
||||
|
|
||||
|
<ng-container *ngIf="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="disableCustomInterval"> |
||||
|
{{ 'timewindow.disable-custom-interval' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle *ngIf="!quickIntervalOnly" |
||||
|
class="mat-slide" formControlName="hideLastInterval"> |
||||
|
{{ 'timewindow.hide' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<tb-timeinterval |
||||
|
formControlName="timewindowMs" |
||||
|
subscriptSizing="dynamic" |
||||
|
appearance="outline" |
||||
|
[disabledAdvanced]="timewindowForm.get('realtime.disableCustomInterval').value" |
||||
|
[required]="timewindow.selectedTab === timewindowTypes.REALTIME && |
||||
|
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"> |
||||
|
</tb-timeinterval> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
|
||||
|
<div *ngIf="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL" |
||||
|
class="tb-form-row"> |
||||
|
<mat-slide-toggle *ngIf="!quickIntervalOnly" |
||||
|
class="mat-slide" formControlName="hideQuickInterval"> |
||||
|
{{ 'timewindow.hide' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<tb-quick-time-interval |
||||
|
displayLabel="false" |
||||
|
formControlName="quickInterval" |
||||
|
onlyCurrentInterval="true" |
||||
|
subscriptSizing="dynamic" |
||||
|
appearance="outline" |
||||
|
[required]="timewindow.selectedTab === timewindowTypes.REALTIME && |
||||
|
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"> |
||||
|
</tb-quick-time-interval> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section *ngIf="timewindowForm.get('selectedTab').value === timewindowTypes.HISTORY" |
||||
|
formGroupName="history" class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title">{{ 'timewindow.timewindow' | translate }}</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="hideInterval"> |
||||
|
{{ 'timewindow.hide-timewindow-section' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
|
||||
|
<tb-toggle-select appearance="stroked" [options]="historyTimewindowOptions" formControlName="historyType"> |
||||
|
</tb-toggle-select> |
||||
|
|
||||
|
<ng-container *ngIf="timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="disableCustomInterval"> |
||||
|
{{ 'timewindow.disable-custom-interval' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="hideLastInterval"> |
||||
|
{{ 'timewindow.hide' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<tb-timeinterval |
||||
|
formControlName="timewindowMs" |
||||
|
subscriptSizing="dynamic" |
||||
|
appearance="outline" |
||||
|
[disabledAdvanced]="timewindowForm.get('history.disableCustomInterval').value" |
||||
|
[required]="timewindow.selectedTab === timewindowTypes.HISTORY && |
||||
|
timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"> |
||||
|
</tb-timeinterval> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
|
||||
|
<div *ngIf="timewindowForm.get('history.historyType').value === historyTypes.FIXED" class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="hideFixedInterval"> |
||||
|
{{ 'timewindow.hide' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<tb-datetime-period |
||||
|
formControlName="fixedTimewindow" |
||||
|
subscriptSizing="dynamic" |
||||
|
appearance="outline" |
||||
|
class="history-time-input" |
||||
|
[required]="timewindow.selectedTab === timewindowTypes.HISTORY && |
||||
|
timewindowForm.get('history.historyType').value === historyTypes.FIXED"> |
||||
|
</tb-datetime-period> |
||||
|
</div> |
||||
|
|
||||
|
<div *ngIf="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL" class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="hideQuickInterval"> |
||||
|
{{ 'timewindow.hide' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<tb-quick-time-interval |
||||
|
displayLabel="false" |
||||
|
formControlName="quickInterval" |
||||
|
subscriptSizing="dynamic" |
||||
|
appearance="outline" |
||||
|
[required]="timewindow.selectedTab === timewindowTypes.HISTORY && |
||||
|
timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"> |
||||
|
</tb-quick-time-interval> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<ng-container *ngIf="aggregation"> |
||||
|
<section class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title">{{ 'aggregation.aggregation' | translate }}</div> |
||||
|
<div class="tb-form-row column-xs"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="hideAggregation"> |
||||
|
{{ 'timewindow.hide' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<ng-container formGroupName="aggregation"> |
||||
|
<mat-form-field class="flex" subscriptSizing="dynamic" appearance="outline"> |
||||
|
<mat-select formControlName="type"> |
||||
|
<mat-option *ngFor="let aggregation of aggregations" [value]="aggregation"> |
||||
|
{{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section class="tb-form-panel" |
||||
|
*ngIf="timewindowForm.get('aggregation.type').value === aggregationTypes.NONE"> |
||||
|
<div class="tb-form-panel-title">{{ 'aggregation.limit' | translate }}</div> |
||||
|
<div class="tb-form-row column-xs"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="hideAggInterval"> |
||||
|
{{ 'timewindow.hide' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<ng-container formGroupName="aggregation"> |
||||
|
<tb-datapoints-limit formControlName="limit" |
||||
|
[required]="timewindowForm.get('aggregation.type').value === aggregationTypes.NONE"> |
||||
|
</tb-datapoints-limit> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section class="tb-form-panel" [fxShow]="timewindowForm.get('aggregation.type').value !== aggregationTypes.NONE"> |
||||
|
<div class="tb-form-panel-title">{{ 'aggregation.group-interval' | translate }}</div> |
||||
|
|
||||
|
<ng-container *ngIf="timewindow.selectedTab === timewindowTypes.REALTIME"> |
||||
|
<div class="tb-form-row" formGroupName="realtime"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="disableCustomGroupInterval"> |
||||
|
{{ 'timewindow.disable-custom-interval' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs"> |
||||
|
<ng-container formGroupName="realtime"> |
||||
|
<ng-container *ngTemplateOutlet="hideAggInterval"> |
||||
|
</ng-container> |
||||
|
<tb-timeinterval |
||||
|
formControlName="interval" |
||||
|
[min]="minRealtimeAggInterval()" [max]="maxRealtimeAggInterval()" |
||||
|
useCalendarIntervals |
||||
|
subscriptSizing="dynamic" |
||||
|
appearance="outline" |
||||
|
[disabledAdvanced]="timewindowForm.get('realtime.disableCustomGroupInterval').value"> |
||||
|
</tb-timeinterval> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
<ng-container *ngIf="timewindow.selectedTab === timewindowTypes.HISTORY"> |
||||
|
<div class="tb-form-row" formGroupName="history"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="disableCustomGroupInterval"> |
||||
|
{{ 'timewindow.disable-custom-interval' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs"> |
||||
|
<ng-container *ngTemplateOutlet="hideAggInterval"> |
||||
|
</ng-container> |
||||
|
<ng-container formGroupName="history"> |
||||
|
<tb-timeinterval |
||||
|
formControlName="interval" |
||||
|
[min]="minHistoryAggInterval()" [max]="maxHistoryAggInterval()" |
||||
|
useCalendarIntervals |
||||
|
subscriptSizing="dynamic" |
||||
|
appearance="outline" |
||||
|
[disabledAdvanced]="timewindowForm.get('history.disableCustomGroupInterval').value"> |
||||
|
</tb-timeinterval> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-template #hideAggInterval> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="hideAggInterval"> |
||||
|
{{ 'timewindow.hide' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</ng-template> |
||||
|
</section> |
||||
|
</ng-container> |
||||
|
|
||||
|
<section class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title">{{ 'timezone.timezone' | translate }}</div> |
||||
|
<div class="tb-form-row column-xs"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="hideTimezone"> |
||||
|
{{ 'timewindow.hide' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<tb-timezone-select [localBrowserTimezonePlaceholderOnEmpty]="true" |
||||
|
formControlName="timezone" |
||||
|
subscriptSizing="dynamic" |
||||
|
appearance="outline" |
||||
|
displayLabel="false"> |
||||
|
</tb-timezone-select> |
||||
|
</div> |
||||
|
</section> |
||||
|
</div> |
||||
|
<mat-divider></mat-divider> |
||||
|
<div mat-dialog-actions class="tb-dialog-actions tb-flex flex-end no-gap"> |
||||
|
<button type="button" |
||||
|
mat-button |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button type="button" |
||||
|
mat-raised-button |
||||
|
color="primary" |
||||
|
(click)="update()" |
||||
|
[disabled]="(isLoading$ | async) || timewindowForm.invalid || !timewindowForm.dirty"> |
||||
|
{{ 'action.apply' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,37 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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. |
||||
|
*/ |
||||
|
|
||||
|
:host { |
||||
|
.tb-timewindow-form { |
||||
|
width: 600px; |
||||
|
|
||||
|
&.mat-mdc-dialog-content { |
||||
|
overflow: hidden; |
||||
|
padding: 0; |
||||
|
} |
||||
|
|
||||
|
tb-timezone-select { |
||||
|
flex: 1; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host-context(.mat-mdc-dialog-container) { |
||||
|
.tb-timewindow-form { |
||||
|
display: grid; |
||||
|
grid-template-rows: min-content min-content minmax(auto, 1fr) min-content min-content; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,395 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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, Inject, OnDestroy, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
aggregationTranslations, |
||||
|
AggregationType, |
||||
|
DAY, |
||||
|
HistoryWindowType, |
||||
|
historyWindowTypeTranslations, |
||||
|
quickTimeIntervalPeriod, |
||||
|
RealtimeWindowType, |
||||
|
realtimeWindowTypeTranslations, |
||||
|
Timewindow, |
||||
|
TimewindowType |
||||
|
} 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 { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
import { TimeService } from '@core/services/time.service'; |
||||
|
import { isDefined, isDefinedAndNotNull, mergeDeep } from '@core/utils'; |
||||
|
import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
|
||||
|
export interface TimewindowConfigDialogData { |
||||
|
quickIntervalOnly: boolean; |
||||
|
aggregation: boolean; |
||||
|
timewindow: Timewindow; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-timewindow-config-dialog', |
||||
|
templateUrl: './timewindow-config-dialog.component.html', |
||||
|
styleUrls: ['./timewindow-config-dialog.component.scss', './timewindow-form.scss'] |
||||
|
}) |
||||
|
export class TimewindowConfigDialogComponent extends PageComponent implements OnInit, OnDestroy { |
||||
|
|
||||
|
quickIntervalOnly = false; |
||||
|
|
||||
|
aggregation = false; |
||||
|
|
||||
|
timewindow: Timewindow; |
||||
|
|
||||
|
timewindowForm: FormGroup; |
||||
|
|
||||
|
historyTypes = HistoryWindowType; |
||||
|
|
||||
|
realtimeTypes = RealtimeWindowType; |
||||
|
|
||||
|
timewindowTypes = TimewindowType; |
||||
|
|
||||
|
aggregationTypes = AggregationType; |
||||
|
|
||||
|
aggregations = Object.keys(AggregationType); |
||||
|
|
||||
|
aggregationTypesTranslations = aggregationTranslations; |
||||
|
|
||||
|
result: Timewindow; |
||||
|
|
||||
|
timewindowTypeOptions: ToggleHeaderOption[] = [ |
||||
|
{ |
||||
|
name: this.translate.instant('timewindow.realtime'), |
||||
|
value: this.timewindowTypes.REALTIME |
||||
|
}, |
||||
|
{ |
||||
|
name: this.translate.instant('timewindow.history'), |
||||
|
value: this.timewindowTypes.HISTORY |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
realtimeTimewindowOptions: ToggleHeaderOption[] = [ |
||||
|
{ |
||||
|
name: this.translate.instant(realtimeWindowTypeTranslations.get(RealtimeWindowType.INTERVAL)), |
||||
|
value: this.realtimeTypes.INTERVAL |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
historyTimewindowOptions: ToggleHeaderOption[] = [ |
||||
|
{ |
||||
|
name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.LAST_INTERVAL)), |
||||
|
value: this.historyTypes.LAST_INTERVAL |
||||
|
}, |
||||
|
{ |
||||
|
name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.FIXED)), |
||||
|
value: this.historyTypes.FIXED |
||||
|
}, |
||||
|
{ |
||||
|
name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.INTERVAL)), |
||||
|
value: this.historyTypes.INTERVAL |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
realtimeTypeSelectionAvailable: boolean; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor(@Inject(MAT_DIALOG_DATA) public data: TimewindowConfigDialogData, |
||||
|
public dialogRef: MatDialogRef<TimewindowConfigDialogComponent, Timewindow>, |
||||
|
protected store: Store<AppState>, |
||||
|
public fb: FormBuilder, |
||||
|
private timeService: TimeService, |
||||
|
private translate: TranslateService) { |
||||
|
super(store); |
||||
|
this.quickIntervalOnly = data.quickIntervalOnly; |
||||
|
this.aggregation = data.aggregation; |
||||
|
this.timewindow = data.timewindow; |
||||
|
|
||||
|
if (!this.quickIntervalOnly) { |
||||
|
this.realtimeTimewindowOptions.unshift({ |
||||
|
name: this.translate.instant(realtimeWindowTypeTranslations.get(RealtimeWindowType.LAST_INTERVAL)), |
||||
|
value: this.realtimeTypes.LAST_INTERVAL |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
this.realtimeTypeSelectionAvailable = this.realtimeTimewindowOptions.length > 1; |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
const realtime = this.timewindow.realtime; |
||||
|
const history = this.timewindow.history; |
||||
|
const aggregation = this.timewindow.aggregation; |
||||
|
|
||||
|
this.timewindowForm = this.fb.group({ |
||||
|
selectedTab: [isDefined(this.timewindow.selectedTab) ? this.timewindow.selectedTab : TimewindowType.REALTIME], |
||||
|
realtime: this.fb.group({ |
||||
|
realtimeType: [ isDefined(realtime?.realtimeType) ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL ], |
||||
|
timewindowMs: [ isDefined(realtime?.timewindowMs) ? this.timewindow.realtime.timewindowMs : null ], |
||||
|
interval: [ isDefined(realtime?.interval) ? this.timewindow.realtime.interval : null ], |
||||
|
quickInterval: [ isDefined(realtime?.quickInterval) ? this.timewindow.realtime.quickInterval : null ], |
||||
|
disableCustomInterval: [ isDefinedAndNotNull(this.timewindow.realtime?.disableCustomInterval) |
||||
|
? this.timewindow.realtime?.disableCustomInterval : false ], |
||||
|
disableCustomGroupInterval: [ isDefinedAndNotNull(this.timewindow.realtime?.disableCustomGroupInterval) |
||||
|
? this.timewindow.realtime?.disableCustomGroupInterval : false ], |
||||
|
hideInterval: [ isDefinedAndNotNull(this.timewindow.realtime.hideInterval) |
||||
|
? this.timewindow.realtime.hideInterval : false ], |
||||
|
hideLastInterval: [{ |
||||
|
value: isDefinedAndNotNull(this.timewindow.realtime.hideLastInterval) |
||||
|
? this.timewindow.realtime.hideLastInterval : false, |
||||
|
disabled: this.timewindow.realtime.hideInterval |
||||
|
}], |
||||
|
hideQuickInterval: [{ |
||||
|
value: isDefinedAndNotNull(this.timewindow.realtime.hideQuickInterval) |
||||
|
? this.timewindow.realtime.hideQuickInterval : false, |
||||
|
disabled: this.timewindow.realtime.hideInterval |
||||
|
}] |
||||
|
}), |
||||
|
history: this.fb.group({ |
||||
|
historyType: [ isDefined(history?.historyType) ? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL ], |
||||
|
timewindowMs: [ isDefined(history?.timewindowMs) ? this.timewindow.history.timewindowMs : null ], |
||||
|
interval: [ isDefined(history?.interval) ? this.timewindow.history.interval : null ], |
||||
|
fixedTimewindow: [ isDefined(history?.fixedTimewindow) ? this.timewindow.history.fixedTimewindow : null ], |
||||
|
quickInterval: [ isDefined(history?.quickInterval) ? this.timewindow.history.quickInterval : null ], |
||||
|
disableCustomInterval: [ isDefinedAndNotNull(this.timewindow.history?.disableCustomInterval) |
||||
|
? this.timewindow.history?.disableCustomInterval : false ], |
||||
|
disableCustomGroupInterval: [ isDefinedAndNotNull(this.timewindow.history?.disableCustomGroupInterval) |
||||
|
? this.timewindow.history?.disableCustomGroupInterval : false ], |
||||
|
hideInterval: [ isDefinedAndNotNull(this.timewindow.history.hideInterval) |
||||
|
? this.timewindow.history.hideInterval : false ], |
||||
|
hideLastInterval: [{ |
||||
|
value: isDefinedAndNotNull(this.timewindow.history.hideLastInterval) |
||||
|
? this.timewindow.history.hideLastInterval : false, |
||||
|
disabled: this.timewindow.history.hideInterval |
||||
|
}], |
||||
|
hideQuickInterval: [{ |
||||
|
value: isDefinedAndNotNull(this.timewindow.history.hideQuickInterval) |
||||
|
? this.timewindow.history.hideQuickInterval : false, |
||||
|
disabled: this.timewindow.history.hideInterval |
||||
|
}], |
||||
|
hideFixedInterval: [{ |
||||
|
value: isDefinedAndNotNull(this.timewindow.history.hideFixedInterval) |
||||
|
? this.timewindow.history.hideFixedInterval : false, |
||||
|
disabled: this.timewindow.history.hideInterval |
||||
|
}] |
||||
|
}), |
||||
|
aggregation: this.fb.group({ |
||||
|
type: [ isDefined(aggregation?.type) ? this.timewindow.aggregation.type : null ], |
||||
|
limit: [ isDefined(aggregation?.limit) ? this.timewindow.aggregation.limit : null ] |
||||
|
}), |
||||
|
timezone: [ isDefined(this.timewindow.timezone) ? this.timewindow.timezone : null ], |
||||
|
hideAggregation: [ isDefinedAndNotNull(this.timewindow.hideAggregation) |
||||
|
? this.timewindow.hideAggregation : false ], |
||||
|
hideAggInterval: [ isDefinedAndNotNull(this.timewindow.hideAggInterval) |
||||
|
? this.timewindow.hideAggInterval : false ], |
||||
|
hideTimezone: [ isDefinedAndNotNull(this.timewindow.hideTimezone) |
||||
|
? this.timewindow.hideTimezone : false ] |
||||
|
}); |
||||
|
|
||||
|
this.updateValidators(this.timewindowForm.get('aggregation.type').value); |
||||
|
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); |
||||
|
}); |
||||
|
this.timewindowForm.get('realtime.hideInterval').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((value: boolean) => { |
||||
|
if (value) { |
||||
|
this.timewindowForm.get('realtime.hideLastInterval').disable({emitEvent: false}); |
||||
|
this.timewindowForm.get('realtime.hideQuickInterval').disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.timewindowForm.get('realtime.hideLastInterval').enable({emitEvent: false}); |
||||
|
this.timewindowForm.get('realtime.hideQuickInterval').enable({emitEvent: false}); |
||||
|
} |
||||
|
}); |
||||
|
this.timewindowForm.get('realtime.hideLastInterval').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((hideLastInterval: boolean) => { |
||||
|
if (hideLastInterval && !this.timewindowForm.get('realtime.hideQuickInterval').value) { |
||||
|
this.timewindowForm.get('realtime.realtimeType').setValue(RealtimeWindowType.INTERVAL); |
||||
|
} |
||||
|
}); |
||||
|
this.timewindowForm.get('realtime.hideQuickInterval').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((hideQuickInterval: boolean) => { |
||||
|
if (hideQuickInterval && !this.timewindowForm.get('realtime.hideLastInterval').value) { |
||||
|
this.timewindowForm.get('realtime.realtimeType').setValue(RealtimeWindowType.LAST_INTERVAL); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
this.timewindowForm.get('history.hideInterval').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((value: boolean) => { |
||||
|
if (value) { |
||||
|
this.timewindowForm.get('history.hideLastInterval').disable({emitEvent: false}); |
||||
|
this.timewindowForm.get('history.hideQuickInterval').disable({emitEvent: false}); |
||||
|
this.timewindowForm.get('history.hideFixedInterval').disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.timewindowForm.get('history.hideLastInterval').enable({emitEvent: false}); |
||||
|
this.timewindowForm.get('history.hideQuickInterval').enable({emitEvent: false}); |
||||
|
this.timewindowForm.get('history.hideFixedInterval').enable({emitEvent: false}); |
||||
|
} |
||||
|
}); |
||||
|
this.timewindowForm.get('history.hideLastInterval').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((hideLastInterval: boolean) => { |
||||
|
if (hideLastInterval) { |
||||
|
if (!this.timewindowForm.get('history.hideFixedInterval').value) { |
||||
|
this.timewindowForm.get('history.historyType').setValue(HistoryWindowType.FIXED); |
||||
|
} else if (!this.timewindowForm.get('history.hideQuickInterval').value) { |
||||
|
this.timewindowForm.get('history.historyType').setValue(HistoryWindowType.INTERVAL); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
this.timewindowForm.get('history.hideFixedInterval').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((hideFixedInterval: boolean) => { |
||||
|
if (hideFixedInterval) { |
||||
|
if (!this.timewindowForm.get('history.hideLastInterval').value) { |
||||
|
this.timewindowForm.get('history.historyType').setValue(HistoryWindowType.LAST_INTERVAL); |
||||
|
} else if (!this.timewindowForm.get('history.hideQuickInterval').value) { |
||||
|
this.timewindowForm.get('history.historyType').setValue(HistoryWindowType.INTERVAL); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
this.timewindowForm.get('history.hideQuickInterval').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((hideQuickInterval: boolean) => { |
||||
|
if (hideQuickInterval) { |
||||
|
if (!this.timewindowForm.get('history.hideLastInterval').value) { |
||||
|
this.timewindowForm.get('history.historyType').setValue(HistoryWindowType.LAST_INTERVAL); |
||||
|
} else if (!this.timewindowForm.get('history.hideFixedInterval').value) { |
||||
|
this.timewindowForm.get('history.historyType').setValue(HistoryWindowType.FIXED); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
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) { |
||||
|
const timewindowFormValue = this.timewindowForm.getRawValue(); |
||||
|
if (selectedTab === TimewindowType.REALTIME) { |
||||
|
if (timewindowFormValue.history.historyType !== HistoryWindowType.FIXED |
||||
|
&& !(this.quickIntervalOnly && timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL)) { |
||||
|
|
||||
|
this.timewindowForm.get('realtime').patchValue({ |
||||
|
realtimeType: Object.keys(RealtimeWindowType).includes(HistoryWindowType[timewindowFormValue.history.historyType]) ? |
||||
|
RealtimeWindowType[HistoryWindowType[timewindowFormValue.history.historyType]] : |
||||
|
timewindowFormValue.realtime.realtimeType, |
||||
|
timewindowMs: timewindowFormValue.history.timewindowMs, |
||||
|
quickInterval: timewindowFormValue.history.quickInterval.startsWith('CURRENT') ? |
||||
|
timewindowFormValue.history.quickInterval : timewindowFormValue.realtime.quickInterval |
||||
|
}); |
||||
|
setTimeout(() => this.timewindowForm.get('realtime.interval').patchValue(timewindowFormValue.history.interval)); |
||||
|
} |
||||
|
} else { |
||||
|
this.timewindowForm.get('history').patchValue({ |
||||
|
historyType: HistoryWindowType[RealtimeWindowType[timewindowFormValue.realtime.realtimeType]], |
||||
|
timewindowMs: timewindowFormValue.realtime.timewindowMs, |
||||
|
quickInterval: timewindowFormValue.realtime.quickInterval |
||||
|
}); |
||||
|
setTimeout(() => this.timewindowForm.get('history.interval').patchValue(timewindowFormValue.realtime.interval)); |
||||
|
} |
||||
|
this.timewindowForm.patchValue({ |
||||
|
aggregation: { |
||||
|
type: timewindowFormValue.aggregation.type, |
||||
|
limit: timewindowFormValue.aggregation.limit |
||||
|
}, |
||||
|
timezone: timewindowFormValue.timezone, |
||||
|
hideAggregation: timewindowFormValue.hideAggregation, |
||||
|
hideAggInterval: timewindowFormValue.hideAggInterval, |
||||
|
hideTimezone: timewindowFormValue.hideTimezone |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
update() { |
||||
|
const timewindowFormValue = this.timewindowForm.getRawValue(); |
||||
|
this.timewindow = mergeDeep(this.timewindow, timewindowFormValue); |
||||
|
if (!this.aggregation) { |
||||
|
delete this.timewindow.aggregation; |
||||
|
} |
||||
|
this.dialogRef.close(this.timewindow); |
||||
|
} |
||||
|
|
||||
|
cancel() { |
||||
|
this.dialogRef.close(); |
||||
|
} |
||||
|
|
||||
|
minRealtimeAggInterval() { |
||||
|
return this.timeService.minIntervalLimit(this.currentRealtimeTimewindow()); |
||||
|
} |
||||
|
|
||||
|
maxRealtimeAggInterval() { |
||||
|
return this.timeService.maxIntervalLimit(this.currentRealtimeTimewindow()); |
||||
|
} |
||||
|
|
||||
|
currentRealtimeTimewindow(): number { |
||||
|
const timeWindowFormValue = this.timewindowForm.getRawValue(); |
||||
|
switch (timeWindowFormValue.realtime.realtimeType) { |
||||
|
case RealtimeWindowType.LAST_INTERVAL: |
||||
|
return timeWindowFormValue.realtime.timewindowMs; |
||||
|
case RealtimeWindowType.INTERVAL: |
||||
|
return quickTimeIntervalPeriod(timeWindowFormValue.realtime.quickInterval); |
||||
|
default: |
||||
|
return DAY; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
minHistoryAggInterval() { |
||||
|
return this.timeService.minIntervalLimit(this.currentHistoryTimewindow()); |
||||
|
} |
||||
|
|
||||
|
maxHistoryAggInterval() { |
||||
|
return this.timeService.maxIntervalLimit(this.currentHistoryTimewindow()); |
||||
|
} |
||||
|
|
||||
|
currentHistoryTimewindow() { |
||||
|
const timewindowFormValue = this.timewindowForm.getRawValue(); |
||||
|
if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { |
||||
|
return timewindowFormValue.history.timewindowMs; |
||||
|
} else if (timewindowFormValue.history.historyType === HistoryWindowType.INTERVAL) { |
||||
|
return quickTimeIntervalPeriod(timewindowFormValue.history.quickInterval); |
||||
|
} else if (timewindowFormValue.history.fixedTimewindow) { |
||||
|
return timewindowFormValue.history.fixedTimewindow.endTimeMs - |
||||
|
timewindowFormValue.history.fixedTimewindow.startTimeMs; |
||||
|
} else { |
||||
|
return DAY; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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. |
||||
|
*/ |
||||
|
|
||||
|
:host { |
||||
|
background-color: #fff; |
||||
|
|
||||
|
.tb-timewindow-form { |
||||
|
overflow: hidden; |
||||
|
|
||||
|
.tb-flex { |
||||
|
gap: 16px; |
||||
|
} |
||||
|
|
||||
|
&-content { |
||||
|
overflow-y: auto; |
||||
|
|
||||
|
tb-timeinterval, |
||||
|
tb-quick-time-interval, |
||||
|
tb-datetime-period, |
||||
|
tb-datapoints-limit { |
||||
|
flex: 1; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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. |
||||
|
|
||||
|
--> |
||||
|
<form [formGroup]="timezoneForm"> |
||||
|
<section class="tb-form-row column-xs space-between"> |
||||
|
<div>{{ 'timezone.timezone' | translate }}</div> |
||||
|
<tb-timezone-select [localBrowserTimezonePlaceholderOnEmpty]="localBrowserTimezonePlaceholderOnEmpty" |
||||
|
formControlName="timezone" |
||||
|
subscriptSizing="dynamic" |
||||
|
appearance="outline" |
||||
|
displayLabel="false" class="flex"> |
||||
|
</tb-timezone-select> |
||||
|
</section> |
||||
|
</form> |
||||
|
<div class="tb-timezone-panel-actions tb-flex flex-end no-gap"> |
||||
|
<button type="button" |
||||
|
mat-button |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button type="button" |
||||
|
mat-raised-button |
||||
|
color="primary" |
||||
|
(click)="update()" |
||||
|
[disabled]="(isLoading$ | async) || timezoneForm.invalid || !timezoneForm.dirty"> |
||||
|
{{ 'action.apply' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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. |
||||
|
*/ |
||||
|
|
||||
|
:host { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
width: 380px; |
||||
|
max-height: 100%; |
||||
|
max-width: 100%; |
||||
|
background-color: #fff; |
||||
|
|
||||
|
tb-timezone-select { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.tb-timezone-panel-actions { |
||||
|
padding-top: 12px; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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, Input, OnInit } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { FormBuilder, FormGroup } from '@angular/forms'; |
||||
|
import { TbPopoverComponent } from '@shared/components/popover.component'; |
||||
|
|
||||
|
export interface TimezoneSelectionResult { |
||||
|
timezone: string | null; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-timezone-panel', |
||||
|
templateUrl: './timezone-panel.component.html', |
||||
|
styleUrls: ['./timezone-panel.component.scss'] |
||||
|
}) |
||||
|
export class TimezonePanelComponent extends PageComponent implements OnInit { |
||||
|
|
||||
|
@Input() |
||||
|
timezone: string | null; |
||||
|
|
||||
|
@Input() |
||||
|
userTimezoneByDefault: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
localBrowserTimezonePlaceholderOnEmpty: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
defaultTimezone: string; |
||||
|
|
||||
|
@Input() |
||||
|
onClose: (result: TimezoneSelectionResult | null) => void; |
||||
|
|
||||
|
@Input() |
||||
|
popoverComponent: TbPopoverComponent; |
||||
|
|
||||
|
timezoneForm: FormGroup; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
public fb: FormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.timezoneForm = this.fb.group({ |
||||
|
timezone: [this.timezone] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
update() { |
||||
|
if (this.onClose) { |
||||
|
this.onClose({ |
||||
|
timezone: this.timezoneForm.get('timezone').value |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
cancel() { |
||||
|
if (this.onClose) { |
||||
|
this.onClose(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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. |
||||
|
|
||||
|
--> |
||||
|
<button *ngIf="asButton && !strokedButton && !flatButton" |
||||
|
[disabled]="timezoneDisabled" |
||||
|
[matTooltip]="tooltipValue()" |
||||
|
[matTooltipPosition]="tooltipPosition" |
||||
|
type="button" |
||||
|
mat-raised-button color="primary" |
||||
|
(click)="toggleTimezone($event)"> |
||||
|
<span>{{displayValue()}}</span> |
||||
|
</button> |
||||
|
<button *ngIf="asButton && strokedButton" |
||||
|
[disabled]="timezoneDisabled" |
||||
|
[matTooltip]="tooltipValue()" |
||||
|
[matTooltipPosition]="tooltipPosition" |
||||
|
type="button" |
||||
|
mat-stroked-button color="primary" |
||||
|
(click)="toggleTimezone($event)"> |
||||
|
<span>{{displayValue()}}</span> |
||||
|
</button> |
||||
|
<button *ngIf="asButton && flatButton" |
||||
|
[disabled]="timezoneDisabled" |
||||
|
[matTooltip]="tooltipValue()" |
||||
|
[matTooltipPosition]="tooltipPosition" |
||||
|
type="button" |
||||
|
mat-button |
||||
|
(click)="toggleTimezone($event)"> |
||||
|
<span>{{displayValue()}}</span> |
||||
|
</button> |
||||
|
<section *ngIf="!asButton" |
||||
|
class="tb-timezone" |
||||
|
[class]="{'no-padding': noPadding}" |
||||
|
[matTooltip]="tooltipValue()" |
||||
|
[matTooltipPosition]="tooltipPosition" |
||||
|
(click)="toggleTimezone($event)"> |
||||
|
<div class="tb-timezone-label" [fxHide]="hideLabel"> |
||||
|
{{displayValue()}} |
||||
|
</div> |
||||
|
</section> |
||||
@ -0,0 +1,64 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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. |
||||
|
*/ |
||||
|
:host { |
||||
|
min-width: 88px; |
||||
|
margin: 8px 0; |
||||
|
max-width: 100%; |
||||
|
&.no-margin { |
||||
|
margin: 0; |
||||
|
} |
||||
|
.mdc-button { |
||||
|
max-width: 100%; |
||||
|
} |
||||
|
section.tb-timezone { |
||||
|
padding: 0 8px; |
||||
|
&.no-padding { |
||||
|
padding: 0; |
||||
|
} |
||||
|
line-height: 32px; |
||||
|
pointer-events: all; |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
place-content: center flex-start; |
||||
|
align-items: center; |
||||
|
gap: 4px; |
||||
|
width: 100%; |
||||
|
|
||||
|
.tb-timezone-label { |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.timezone-abbr { |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.mdc-button { |
||||
|
.mat-icon { |
||||
|
min-width: 24px; |
||||
|
} |
||||
|
.mdc-button__label { |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,235 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
forwardRef, |
||||
|
HostBinding, |
||||
|
Input, |
||||
|
OnInit, |
||||
|
Renderer2, |
||||
|
ViewContainerRef |
||||
|
} from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { TooltipPosition } from '@angular/material/tooltip'; |
||||
|
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coercion'; |
||||
|
import { TimezonePanelComponent, TimezoneSelectionResult } from '@shared/components/time/timezone-panel.component'; |
||||
|
import { TbPopoverService } from '@shared/components/popover.service'; |
||||
|
import { getTimezoneInfo, TimezoneInfo } from '@shared/models/time/time.models'; |
||||
|
import { TimeService } from '@core/services/time.service'; |
||||
|
|
||||
|
// @dynamic
|
||||
|
@Component({ |
||||
|
selector: 'tb-timezone', |
||||
|
templateUrl: './timezone.component.html', |
||||
|
styleUrls: ['./timezone.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => TimezoneComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class TimezoneComponent implements ControlValueAccessor, OnInit { |
||||
|
|
||||
|
@HostBinding('class.no-margin') |
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
noMargin = false; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
noPadding = false; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
disablePanel = false; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
asButton = false; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
strokedButton = false; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
flatButton = false; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
displayTimezoneValue = true; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
hideLabel = false; |
||||
|
|
||||
|
@Input() |
||||
|
tooltipPosition: TooltipPosition = 'above'; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
private userTimezoneByDefaultValue: boolean; |
||||
|
get userTimezoneByDefault(): boolean { |
||||
|
return this.userTimezoneByDefaultValue; |
||||
|
} |
||||
|
@Input() |
||||
|
set userTimezoneByDefault(value: boolean) { |
||||
|
this.userTimezoneByDefaultValue = coerceBooleanProperty(value); |
||||
|
} |
||||
|
|
||||
|
private localBrowserTimezonePlaceholderOnEmptyValue: boolean; |
||||
|
get localBrowserTimezonePlaceholderOnEmpty(): boolean { |
||||
|
return this.localBrowserTimezonePlaceholderOnEmptyValue; |
||||
|
} |
||||
|
@Input() |
||||
|
set localBrowserTimezonePlaceholderOnEmpty(value: boolean) { |
||||
|
this.localBrowserTimezonePlaceholderOnEmptyValue = coerceBooleanProperty(value); |
||||
|
} |
||||
|
defaultTimezoneId: string = null; |
||||
|
|
||||
|
@Input() |
||||
|
set defaultTimezone(timezone: string) { |
||||
|
if (this.defaultTimezoneId !== timezone) { |
||||
|
this.defaultTimezoneId = timezone; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private requiredValue: boolean; |
||||
|
get required(): boolean { |
||||
|
return this.requiredValue; |
||||
|
} |
||||
|
@Input() |
||||
|
set required(value: boolean) { |
||||
|
this.requiredValue = coerceBooleanProperty(value); |
||||
|
} |
||||
|
|
||||
|
modelValue: string | null; |
||||
|
timezoneInfo: TimezoneInfo; |
||||
|
|
||||
|
private localBrowserTimezoneInfoPlaceholder: TimezoneInfo = this.timeService.getLocalBrowserTimezoneInfoPlaceholder(); |
||||
|
|
||||
|
timezoneDisabled: boolean; |
||||
|
|
||||
|
private propagateChange = (_: any) => {}; |
||||
|
|
||||
|
constructor(private translate: TranslateService, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
public viewContainerRef: ViewContainerRef, |
||||
|
private popoverService: TbPopoverService, |
||||
|
private renderer: Renderer2, |
||||
|
private timeService: TimeService) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
} |
||||
|
|
||||
|
toggleTimezone($event: Event) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
if (this.disablePanel) { |
||||
|
return; |
||||
|
} |
||||
|
const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; |
||||
|
if (this.popoverService.hasPopover(trigger)) { |
||||
|
this.popoverService.hidePopover(trigger); |
||||
|
} else { |
||||
|
const timezoneSelectionPopover = this.popoverService.displayPopover(trigger, this.renderer, |
||||
|
this.viewContainerRef, TimezonePanelComponent, ['bottomRight', 'leftBottom'], true, null, |
||||
|
{ |
||||
|
timezone: this.modelValue, |
||||
|
userTimezoneByDefault: this.userTimezoneByDefaultValue, |
||||
|
localBrowserTimezonePlaceholderOnEmpty: this.localBrowserTimezonePlaceholderOnEmptyValue, |
||||
|
defaultTimezone: this.defaultTimezoneId, |
||||
|
onClose: (result: TimezoneSelectionResult | null) => { |
||||
|
timezoneSelectionPopover.hide(); |
||||
|
if (result) { |
||||
|
this.modelValue = result.timezone; |
||||
|
this.setTimezoneInfo(); |
||||
|
this.timezoneDisabled = this.isTimezoneDisabled(); |
||||
|
this.updateDisplayValue(); |
||||
|
this.notifyChanged(); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{}, |
||||
|
{}, {}, false); |
||||
|
timezoneSelectionPopover.tbComponentRef.instance.popoverComponent = timezoneSelectionPopover; |
||||
|
} |
||||
|
this.cd.detectChanges(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
this.timezoneDisabled = this.isTimezoneDisabled(); |
||||
|
} |
||||
|
|
||||
|
writeValue(value: string | null): void { |
||||
|
this.modelValue = value; |
||||
|
this.setTimezoneInfo(); |
||||
|
this.timezoneDisabled = this.isTimezoneDisabled(); |
||||
|
this.updateDisplayValue(); |
||||
|
} |
||||
|
|
||||
|
notifyChanged() { |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
|
||||
|
displayValue(): string { |
||||
|
return this.displayTimezoneValue && this.timezoneInfo ? this.timezoneInfo.offset : this.translate.instant('timezone.timezone'); |
||||
|
} |
||||
|
|
||||
|
tooltipValue(): string { |
||||
|
return this.timezoneInfo ? `${this.timezoneInfo.name} (${this.timezoneInfo.offset})` : undefined; |
||||
|
} |
||||
|
|
||||
|
updateDisplayValue() { |
||||
|
this.cd.detectChanges(); |
||||
|
} |
||||
|
|
||||
|
private isTimezoneDisabled(): boolean { |
||||
|
return this.disabled; |
||||
|
} |
||||
|
|
||||
|
private setTimezoneInfo() { |
||||
|
const foundTimezone = getTimezoneInfo(this.modelValue, this.defaultTimezoneId, this.userTimezoneByDefaultValue); |
||||
|
if (foundTimezone !== null) { |
||||
|
this.timezoneInfo = foundTimezone; |
||||
|
} else { |
||||
|
if (this.localBrowserTimezonePlaceholderOnEmptyValue) { |
||||
|
this.timezoneInfo = this.localBrowserTimezoneInfoPlaceholder; |
||||
|
} else { |
||||
|
this.timezoneInfo = null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue