Browse Source

Merge pull request #11633 from ChantsovaEkaterina/feature/extend-timewindow-config

Timewindow redesign
pull/11645/head
Igor Kulikov 2 years ago
committed by GitHub
parent
commit
6a3aba914d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 25
      ui-ngx/src/app/core/services/time.service.ts
  2. 6
      ui-ngx/src/app/modules/common/modules-map.ts
  3. 30
      ui-ngx/src/app/shared/components/time/datapoints-limit.component.html
  4. 38
      ui-ngx/src/app/shared/components/time/datapoints-limit.component.scss
  5. 160
      ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts
  6. 36
      ui-ngx/src/app/shared/components/time/datetime-period.component.html
  7. 11
      ui-ngx/src/app/shared/components/time/datetime-period.component.scss
  8. 7
      ui-ngx/src/app/shared/components/time/datetime-period.component.ts
  9. 4
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.html
  10. 12
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts
  11. 55
      ui-ngx/src/app/shared/components/time/timeinterval.component.html
  12. 15
      ui-ngx/src/app/shared/components/time/timeinterval.component.scss
  13. 257
      ui-ngx/src/app/shared/components/time/timeinterval.component.ts
  14. 269
      ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html
  15. 37
      ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.scss
  16. 395
      ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts
  17. 38
      ui-ngx/src/app/shared/components/time/timewindow-form.scss
  18. 392
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  19. 67
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss
  20. 423
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
  21. 1
      ui-ngx/src/app/shared/components/time/timewindow.component.html
  22. 14
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  23. 43
      ui-ngx/src/app/shared/components/time/timezone-panel.component.html
  24. 33
      ui-ngx/src/app/shared/components/time/timezone-panel.component.scss
  25. 80
      ui-ngx/src/app/shared/components/time/timezone-panel.component.ts
  26. 5
      ui-ngx/src/app/shared/components/time/timezone-select.component.html
  27. 38
      ui-ngx/src/app/shared/components/time/timezone-select.component.ts
  28. 54
      ui-ngx/src/app/shared/components/time/timezone.component.html
  29. 64
      ui-ngx/src/app/shared/components/time/timezone.component.scss
  30. 235
      ui-ngx/src/app/shared/components/time/timezone.component.ts
  31. 2
      ui-ngx/src/app/shared/models/overlay.models.ts
  32. 3
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  33. 96
      ui-ngx/src/app/shared/models/time/time.models.ts
  34. 12
      ui-ngx/src/app/shared/shared.module.ts
  35. 12
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  36. 44
      ui-ngx/src/form.scss

25
ui-ngx/src/app/core/services/time.service.ts

@ -19,13 +19,18 @@ import {
AggregationType,
DAY,
defaultTimeIntervals,
defaultTimewindow, Interval, IntervalMath,
defaultTimewindow,
getDefaultTimezoneInfo,
Interval,
IntervalMath,
SECOND,
TimeInterval,
Timewindow
Timewindow,
TimezoneInfo
} from '@shared/models/time/time.models';
import { HttpClient } from '@angular/common/http';
import { isDefined } from '@core/utils';
import { deepClone, isDefined } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
const MIN_INTERVAL = SECOND;
const MAX_INTERVAL = 365 * 20 * DAY;
@ -41,8 +46,11 @@ export class TimeService {
private maxDatapointsLimit = MAX_DATAPOINTS_LIMIT;
private localBrowserTimezoneInfoPlaceholder: TimezoneInfo;
constructor(
private http: HttpClient
private http: HttpClient,
private translate: TranslateService
) {}
public setMaxDatapointsLimit(limit: number) {
@ -161,4 +169,13 @@ export class TimeService {
return defValue;
}
}
public getLocalBrowserTimezoneInfoPlaceholder(): TimezoneInfo {
if (!this.localBrowserTimezoneInfoPlaceholder) {
this.localBrowserTimezoneInfoPlaceholder = deepClone(getDefaultTimezoneInfo());
this.localBrowserTimezoneInfoPlaceholder.id = null;
this.localBrowserTimezoneInfoPlaceholder.name = this.translate.instant('timezone.browser-time');
}
return this.localBrowserTimezoneInfoPlaceholder;
}
}

6
ui-ngx/src/app/modules/common/modules-map.ts

@ -335,6 +335,9 @@ import * as AssetProfileAutocompleteComponent from '@home/components/profile/ass
import * as RuleChainSelectComponent from '@shared/components/rule-chain/rule-chain-select.component';
import { IModulesMap } from '@modules/common/modules-map.models';
import { TimezoneComponent } from '@shared/components/time/timezone.component';
import { TimezonePanelComponent } from '@shared/components/time/timezone-panel.component';
import { DatapointsLimitComponent } from '@shared/components/time/datapoints-limit.component';
declare const System;
@ -466,6 +469,9 @@ class ModulesMap implements IModulesMap {
'@shared/components/time/datetime-period.component': DatetimePeriodComponent,
'@shared/components/time/datetime.component': DatetimeComponent,
'@shared/components/time/timezone-select.component': TimezoneSelectComponent,
'@shared/components/time/timezone.component': TimezoneComponent,
'@shared/components/time/timezone-panel.component': TimezonePanelComponent,
'@shared/components/time/datapoints-limit': DatapointsLimitComponent,
'@shared/components/value-input.component': ValueInputComponent,
'@shared/components/dashboard-autocomplete.component': DashboardAutocompleteComponent,
'@shared/components/entity/entity-subtype-autocomplete.component': EntitySubTypeAutocompleteComponent,

30
ui-ngx/src/app/shared/components/time/datapoints-limit.component.html

@ -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>

38
ui-ngx/src/app/shared/components/time/datapoints-limit.component.scss

@ -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%;
}
}
}

160
ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts

@ -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();
}
}

36
ui-ngx/src/app/shared/components/time/datetime-period.component.html

@ -15,35 +15,17 @@
limitations under the License.
-->
<section fxLayout="column" fxLayoutAlign="start stretch">
<section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"
fxLayout.xs="column" fxLayoutAlign.xs="start stretch" fxLayoutGap.xs="0">
<mat-form-field>
<mat-label translate>datetime.date-from</mat-label>
<mat-datetimepicker-toggle [for]="startDatePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startDatePicker type="date" openOnFocus="true"></mat-datetimepicker>
<section class="tb-form-row column-xs no-border no-padding tb-standard-fields">
<mat-form-field class="flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label translate>datetime.from</mat-label>
<mat-datetimepicker-toggle [for]="startDatePicker" matSuffix></mat-datetimepicker-toggle>
<mat-datetimepicker #startDatePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
<input matInput [disabled]="disabled" [(ngModel)]="startDate" [matDatetimepicker]="startDatePicker" (ngModelChange)="onStartDateChange()">
</mat-form-field>
<mat-form-field>
<mat-label translate>datetime.time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input matInput [disabled]="disabled" [(ngModel)]="startDate" [matDatetimepicker]="startTimePicker" (ngModelChange)="onStartDateChange()">
</mat-form-field>
</section>
<section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"
fxLayout.xs="column" fxLayoutAlign.xs="start stretch" fxLayoutGap.xs="0">
<mat-form-field>
<mat-label translate>datetime.date-to</mat-label>
<mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endDatePicker type="date" openOnFocus="true"></mat-datetimepicker>
<mat-form-field class="flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label translate>datetime.to</mat-label>
<mat-datetimepicker-toggle [for]="endDatePicker" matSuffix></mat-datetimepicker-toggle>
<mat-datetimepicker #endDatePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
<input matInput [disabled]="disabled" [(ngModel)]="endDate" [matDatetimepicker]="endDatePicker" (ngModelChange)="onEndDateChange()">
</mat-form-field>
<mat-form-field>
<mat-label translate>datetime.time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input matInput [disabled]="disabled" [(ngModel)]="endDate" [matDatetimepicker]="endTimePicker" (ngModelChange)="onEndDateChange()">
</mat-form-field>
</section>
</section>

11
ui-ngx/src/app/shared/components/time/datetime-period.component.scss

@ -13,14 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../scss/constants';
:host ::ng-deep {
.mat-mdc-form-field-infix {
width: 100px;
@media #{$mat-xs} {
width: 100%;
}
:host {
.tb-form-row {
gap: 8px;
}
}

7
ui-ngx/src/app/shared/components/time/datetime-period.component.ts

@ -17,6 +17,7 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { FixedWindow } from '@shared/models/time/time.models';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
@Component({
selector: 'tb-datetime-period',
@ -34,6 +35,12 @@ export class DatetimePeriodComponent implements OnInit, ControlValueAccessor {
@Input() disabled: boolean;
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
modelValue: FixedWindow;
startDate: Date;

4
ui-ngx/src/app/shared/components/time/quick-time-interval.component.html

@ -16,8 +16,8 @@
-->
<section class="interval-section" fxLayout="row" fxFlex>
<mat-form-field fxFlex>
<mat-label translate>timewindow.interval</mat-label>
<mat-form-field fxFlex [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label *ngIf="displayLabel" translate>timewindow.interval</mat-label>
<mat-select [disabled]="disabled" [(ngModel)]="modelValue" (ngModelChange)="onIntervalChange()">
<mat-option *ngFor="let interval of intervals" [value]="interval">
{{ timeIntervalTranslationMap.get(interval) | translate}}

12
ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts

@ -17,6 +17,8 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/models/time/time.models';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-quick-time-interval',
@ -39,10 +41,20 @@ export class QuickTimeIntervalComponent implements OnInit, ControlValueAccessor
rendered = false;
@Input()
@coerceBoolean()
displayLabel = true;
@Input() disabled: boolean;
@Input() onlyCurrentInterval = false;
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
private propagateChange = (_: any) => {};
constructor() {

55
ui-ngx/src/app/shared/components/time/timeinterval.component.html

@ -15,43 +15,40 @@
limitations under the License.
-->
<section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="8px">
<section fxLayout="column" fxLayoutAlign="start center" [fxShow]="isEdit">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [(ngModel)]="hideFlag" (ngModelChange)="onHideFlagChange()"></mat-checkbox>
<section class="tb-form-panel no-border no-padding" [formGroup]="timeintervalFormGroup">
<section class="tb-form-row column-xs no-border no-padding tb-standard-fields">
<ng-content></ng-content>
<mat-form-field fxFlex class="flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label *ngIf="predefinedName" translate>{{ predefinedName }}</mat-label>
<mat-select formControlName="interval">
<mat-option *ngFor="let interval of intervals" [value]="interval.value">
{{ interval.name | translate:interval.translateParams }}
</mat-option>
</mat-select>
</mat-form-field>
</section>
<section class="interval-section" fxLayout="column" fxFlex [fxShow]="advanced && (isEdit || !hideFlag)">
<section fxLayout="row wrap" fxLayoutAlign="start start" fxFlex fxLayoutGap="6px">
<mat-form-field class="number-input">
<section class="tb-form-row column-xs no-border no-padding tb-standard-fields advanced-input"
formGroupName="customInterval"
[fxShow]="advanced">
<div class="tb-flex">
<mat-form-field class="number-input flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label translate>timeinterval.days</mat-label>
<input matInput [disabled]="hideFlag || disabled" type="number" step="1" min="0" [(ngModel)]="days" (ngModelChange)="onTimeInputChange('days')"/>
<input matInput type="number" step="1" min="0" formControlName="days" />
</mat-form-field>
<mat-form-field class="number-input">
<mat-form-field class="number-input flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label translate>timeinterval.hours</mat-label>
<input matInput [disabled]="hideFlag || disabled" type="number" step="1" [(ngModel)]="hours" (ngModelChange)="onTimeInputChange('hours')"/>
<input matInput type="number" step="1" formControlName="hours" />
</mat-form-field>
<mat-form-field class="number-input">
</div>
<div class="tb-flex">
<mat-form-field class="number-input flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label translate>timeinterval.minutes</mat-label>
<input matInput [disabled]="hideFlag || disabled" type="number" step="1" [(ngModel)]="mins" (ngModelChange)="onTimeInputChange('mins')"/>
<input matInput type="number" step="1" formControlName="mins" />
</mat-form-field>
<mat-form-field class="number-input">
<mat-form-field class="number-input flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label translate>timeinterval.seconds</mat-label>
<input matInput [disabled]="hideFlag || disabled" type="number" step="1" [(ngModel)]="secs" (ngModelChange)="onTimeInputChange('secs')"/>
<input matInput type="number" step="1" formControlName="secs" />
</mat-form-field>
</section>
</section>
<section fxLayout="row" fxFlex [fxShow]="!advanced && (isEdit || !hideFlag)">
<mat-form-field fxFlex [subscriptSizing]="subscriptSizing">
<mat-label *ngIf="predefinedName" translate>{{ predefinedName }}</mat-label>
<mat-select [disabled]="hideFlag || disabled" [(ngModel)]="interval" (ngModelChange)="onIntervalChange()" style="min-width: 150px;">
<mat-option *ngFor="let interval of intervals" [value]="interval.value">
{{ interval.name | translate:interval.translateParams }}
</mat-option>
</mat-select>
</mat-form-field>
</section>
<section fxLayout="column" fxLayoutAlign="center center" [fxShow]="(isEdit || !hideFlag) && !disabledAdvanced">
<label class="tb-small advanced-label" translate>timeinterval.advanced</label>
<mat-slide-toggle [disabled]="hideFlag || disabled" class="advanced-switch" [(ngModel)]="advanced" (ngModelChange)="onAdvancedChange()"></mat-slide-toggle>
</div>
</section>
</section>

15
ui-ngx/src/app/shared/components/time/timeinterval.component.scss

@ -16,19 +16,8 @@
@import '../../../../scss/constants';
:host {
min-width: 355px;
.advanced-switch {
margin-bottom: 16px;
}
.advanced-label {
margin: 5px 0;
}
.hide-label {
margin-bottom: 5px;
margin-right: 5px;
.advanced-input {
gap: 8px;
}
@media #{$mat-xs} {

257
ui-ngx/src/app/shared/components/time/timeinterval.component.ts

@ -14,14 +14,17 @@
/// limitations under the License.
///
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TimeService } from '@core/services/time.service';
import { coerceNumberProperty } from '@angular/cdk/coercion';
import { SubscriptSizing } from '@angular/material/form-field';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coercion';
import { Interval, IntervalMath, TimeInterval } from '@shared/models/time/time.models';
import { isDefined } from '@core/utils';
import { IntervalType } from '@shared/models/telemetry/telemetry.models';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@Component({
selector: 'tb-timeinterval',
@ -35,11 +38,13 @@ import { isDefined } from '@core/utils';
}
]
})
export class TimeintervalComponent implements OnInit, ControlValueAccessor {
export class TimeintervalComponent implements OnInit, ControlValueAccessor, OnDestroy {
minValue: number;
maxValue: number;
disabledAdvancedState = false;
@Input()
set min(min: number) {
const minValueData = coerceNumberProperty(min);
@ -68,33 +73,37 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
@Input()
@coerceBoolean()
hideFlag = false;
@Input()
@coerceBoolean()
disabledAdvanced = false;
set disabledAdvanced(disabledAdvanced: boolean) {
if (this.disabledAdvancedState !== disabledAdvanced) {
this.disabledAdvancedState = disabledAdvanced;
this.updateIntervalValue(true);
}
}
@Input()
@coerceBoolean()
useCalendarIntervals = false;
@Output() hideFlagChange = new EventEmitter<boolean>();
@Input() disabled: boolean;
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
days = 0;
hours = 0;
mins = 1;
secs = 0;
@Input()
appearance: MatFormFieldAppearance = 'fill';
interval: Interval = 0;
intervals: Array<TimeInterval>;
advanced = false;
timeintervalFormGroup: FormGroup;
customTimeInterval: TimeInterval = {
name: 'timeinterval.custom',
translateParams: {},
value: IntervalType.CUSTOM
};
private modelValue: Interval;
private rendered = false;
private propagateChangeValue: any;
@ -103,7 +112,43 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
this.propagateChangeValue = value;
};
constructor(private timeService: TimeService) {
private destroy$ = new Subject<void>();
constructor(private timeService: TimeService,
private fb: FormBuilder) {
this.timeintervalFormGroup = this.fb.group({
interval: [ 1 ],
customInterval: this.fb.group({
days: [ 0 ],
hours: [ 0 ],
mins: [ 1 ],
secs: [ 0 ]
})
});
this.timeintervalFormGroup.get('interval').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.onIntervalChange());
this.timeintervalFormGroup.get('customInterval').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateView());
this.timeintervalFormGroup.get('customInterval.secs').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((secs) => this.onSecsChange(secs));
this.timeintervalFormGroup.get('customInterval.mins').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((mins) => this.onMinsChange(mins));
this.timeintervalFormGroup.get('customInterval.hours').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((hours) => this.onHoursChange(hours));
this.timeintervalFormGroup.get('customInterval.days').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((days) => this.onDaysChange(days));
}
ngOnInit(): void {
@ -122,17 +167,36 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.timeintervalFormGroup.disable({emitEvent: false});
} else {
this.timeintervalFormGroup.enable({emitEvent: false});
}
}
writeValue(interval: Interval): void {
this.modelValue = interval;
this.rendered = true;
this.updateIntervalValue();
}
private updateIntervalValue(forceBoundInterval = false) {
if (typeof this.modelValue !== 'undefined') {
const min = this.timeService.boundMinInterval(this.minValue);
const max = this.timeService.boundMaxInterval(this.maxValue);
if (IntervalMath.numberValue(this.modelValue) >= min && IntervalMath.numberValue(this.modelValue) <= max) {
this.advanced = !this.timeService.matchesExistingInterval(this.minValue, this.maxValue, this.modelValue, this.useCalendarIntervals);
this.setInterval(this.modelValue);
const advanced = !this.timeService.matchesExistingInterval(this.minValue, this.maxValue, this.modelValue,
this.useCalendarIntervals);
if (advanced && this.disabledAdvancedState) {
this.advanced = false;
this.boundInterval();
} else {
this.advanced = advanced;
this.setInterval(this.modelValue);
if (forceBoundInterval) {
this.boundInterval();
}
}
} else {
this.boundInterval();
}
@ -141,19 +205,30 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
private setInterval(interval: Interval) {
if (!this.advanced) {
this.interval = interval;
this.timeintervalFormGroup.get('interval').patchValue(interval, {emitEvent: false});
} else {
this.timeintervalFormGroup.get('interval').patchValue(IntervalType.CUSTOM, {emitEvent: false});
this.setCustomInterval(interval);
}
}
private setCustomInterval(interval: Interval) {
const intervalSeconds = Math.floor(IntervalMath.numberValue(interval) / 1000);
this.days = Math.floor(intervalSeconds / 86400);
this.hours = Math.floor((intervalSeconds % 86400) / 3600);
this.mins = Math.floor(((intervalSeconds % 86400) % 3600) / 60);
this.secs = intervalSeconds % 60;
this.timeintervalFormGroup.get('customInterval').patchValue({
days: Math.floor(intervalSeconds / 86400),
hours: Math.floor((intervalSeconds % 86400) / 3600),
mins: Math.floor(((intervalSeconds % 86400) % 3600) / 60),
secs: intervalSeconds % 60
}, {emitEvent: false});
}
private boundInterval(updateToPreferred = false) {
const min = this.timeService.boundMinInterval(this.minValue);
const max = this.timeService.boundMaxInterval(this.maxValue);
this.intervals = this.timeService.getIntervals(this.minValue, this.maxValue, this.useCalendarIntervals);
if (!this.disabledAdvancedState) {
this.intervals.push(this.customTimeInterval);
}
if (this.rendered) {
let newInterval = this.modelValue;
const newIntervalMs = IntervalMath.numberValue(newInterval);
@ -179,7 +254,7 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
let value: Interval = null;
let interval: Interval;
if (!this.advanced) {
interval = this.interval;
interval = this.timeintervalFormGroup.get('interval').value;
if (!interval || typeof interval === 'number' && isNaN(interval)) {
interval = this.calculateIntervalMs();
}
@ -195,118 +270,90 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
}
private calculateIntervalMs(): number {
return (this.days * 86400 +
this.hours * 3600 +
this.mins * 60 +
this.secs) * 1000;
const customInterval = this.timeintervalFormGroup.get('customInterval').value;
return (customInterval.days * 86400 +
customInterval.hours * 3600 +
customInterval.mins * 60 +
customInterval.secs) * 1000;
}
onIntervalChange() {
this.updateView();
}
onAdvancedChange() {
if (!this.advanced) {
this.interval = this.calculateIntervalMs();
} else {
let interval = this.interval;
if (!interval || typeof interval === 'number' && isNaN(interval)) {
interval = this.calculateIntervalMs();
const customIntervalSelected = this.timeintervalFormGroup.get('interval').value === IntervalType.CUSTOM;
if (customIntervalSelected !== this.advanced) {
this.advanced = customIntervalSelected;
if (this.advanced) {
this.setCustomInterval(this.modelValue);
}
this.setInterval(interval);
}
this.updateView();
}
onHideFlagChange() {
this.hideFlagChange.emit(this.hideFlag);
}
onTimeInputChange(type: string) {
switch (type) {
case 'secs':
setTimeout(() => this.onSecsChange(), 0);
break;
case 'mins':
setTimeout(() => this.onMinsChange(), 0);
break;
case 'hours':
setTimeout(() => this.onHoursChange(), 0);
break;
case 'days':
setTimeout(() => this.onDaysChange(), 0);
break;
}
}
private onSecsChange() {
if (typeof this.secs === 'undefined') {
private onSecsChange(secs: number) {
const customInterval = this.timeintervalFormGroup.get('customInterval').value;
if (typeof secs === 'undefined') {
return;
}
if (this.secs < 0) {
if ((this.days + this.hours + this.mins) > 0) {
this.secs = this.secs + 60;
this.mins--;
this.onMinsChange();
if (secs < 0) {
if ((customInterval.days + customInterval.hours + customInterval.mins) > 0) {
this.timeintervalFormGroup.get('customInterval.secs').patchValue(secs + 60, {emitEvent: false});
this.timeintervalFormGroup.get('customInterval.mins').patchValue(customInterval.mins - 1, {emitEvent: true});
} else {
this.secs = 0;
this.timeintervalFormGroup.get('customInterval.secs').patchValue(0, {emitEvent: false});
}
} else if (this.secs >= 60) {
this.secs = this.secs - 60;
this.mins++;
this.onMinsChange();
} else if (secs >= 60) {
this.timeintervalFormGroup.get('customInterval.secs').patchValue(secs - 60, {emitEvent: false});
this.timeintervalFormGroup.get('customInterval.mins').patchValue(customInterval.mins + 1, {emitEvent: true});
}
this.updateView();
}
private onMinsChange() {
if (typeof this.mins === 'undefined') {
private onMinsChange(mins: number) {
const customInterval = this.timeintervalFormGroup.get('customInterval').value;
if (typeof mins === 'undefined') {
return;
}
if (this.mins < 0) {
if ((this.days + this.hours) > 0) {
this.mins = this.mins + 60;
this.hours--;
this.onHoursChange();
if (mins < 0) {
if ((customInterval.days + customInterval.hours) > 0) {
this.timeintervalFormGroup.get('customInterval.mins').patchValue(mins + 60, {emitEvent: false});
this.timeintervalFormGroup.get('customInterval.hours').patchValue(customInterval.hours - 1, {emitEvent: true});
} else {
this.mins = 0;
this.timeintervalFormGroup.get('customInterval.mins').patchValue(0, {emitEvent: false});
}
} else if (this.mins >= 60) {
this.mins = this.mins - 60;
this.hours++;
this.onHoursChange();
} else if (mins >= 60) {
this.timeintervalFormGroup.get('customInterval.mins').patchValue(mins - 60, {emitEvent: false});
this.timeintervalFormGroup.get('customInterval.hours').patchValue(customInterval.hours + 1, {emitEvent: true});
}
this.updateView();
}
private onHoursChange() {
if (typeof this.hours === 'undefined') {
private onHoursChange(hours: number) {
const customInterval = this.timeintervalFormGroup.get('customInterval').value;
if (typeof hours === 'undefined') {
return;
}
if (this.hours < 0) {
if (this.days > 0) {
this.hours = this.hours + 24;
this.days--;
this.onDaysChange();
if (hours < 0) {
if (customInterval.days > 0) {
this.timeintervalFormGroup.get('customInterval.hours').patchValue(hours + 24, {emitEvent: false});
this.timeintervalFormGroup.get('customInterval.days').patchValue(customInterval.days - 1, {emitEvent: true});
} else {
this.hours = 0;
this.timeintervalFormGroup.get('customInterval.hours').patchValue(0, {emitEvent: false});
}
} else if (this.hours >= 24) {
this.hours = this.hours - 24;
this.days++;
this.onDaysChange();
} else if (hours >= 24) {
this.timeintervalFormGroup.get('customInterval.hours').patchValue(hours - 24, {emitEvent: false});
this.timeintervalFormGroup.get('customInterval.days').patchValue(customInterval.days + 1, {emitEvent: true});
}
this.updateView();
}
private onDaysChange() {
if (typeof this.days === 'undefined') {
private onDaysChange(days: number) {
if (typeof days === 'undefined') {
return;
}
if (this.days < 0) {
this.days = 0;
if (days < 0) {
this.timeintervalFormGroup.get('customInterval.days').patchValue(0, {emitEvent: false});
}
this.updateView();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

269
ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html

@ -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>

37
ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.scss

@ -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;
}
}

395
ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts

@ -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;
}
}
}

38
ui-ngx/src/app/shared/components/time/timewindow-form.scss

@ -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;
}
}
}
}

392
ui-ngx/src/app/shared/components/time/timewindow-panel.component.html

@ -15,236 +15,190 @@
limitations under the License.
-->
<form [formGroup]="timewindowForm" class="mat-content">
<mat-tab-group [ngClass]="{'tb-headless': historyOnly}"
(selectedTabChange)="onTimewindowTypeChange()" [(selectedIndex)]="timewindow.selectedTab">
<mat-tab label="{{ 'timewindow.realtime' | translate }}">
<section fxLayout="row">
<section *ngIf="isEdit" fxLayout="column" fxLayoutAlign="start center"
style="padding-top: 8px; padding-left: 16px;">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideInterval"
(ngModelChange)="onHideIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval">
<div formGroupName="realtime" class="mat-content mat-padding" style="padding-top: 8px;">
<mat-radio-group *ngIf="!quickIntervalOnly"
[fxShow]="isEdit || (!timewindow.hideLastInterval && !timewindow.hideQuickInterval)"
formControlName="realtimeType">
<mat-radio-button [value]="realtimeTypes.LAST_INTERVAL" color="primary">
<section fxLayout="row">
<section *ngIf="isEdit" fxLayout="column" fxLayoutAlign="start center" style="padding-right: 8px;">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideLastInterval"
(ngModelChange)="onHideLastIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column">
<span translate>timewindow.last</span>
<tb-timeinterval
formControlName="timewindowMs"
predefinedName="timewindow.last"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
style="padding-top: 8px;"></tb-timeinterval>
</section>
</section>
</mat-radio-button>
<mat-radio-button [value]="realtimeTypes.INTERVAL" color="primary">
<section fxLayout="row">
<section *ngIf="isEdit" fxLayout="column" fxLayoutAlign="start center" style="padding-right: 8px;">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideQuickInterval"
(ngModelChange)="onHideQuickIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column">
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
onlyCurrentInterval="true"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
style="padding-top: 8px"></tb-quick-time-interval>
</section>
</section>
</mat-radio-button>
</mat-radio-group>
<tb-timeinterval *ngIf="!isEdit && !timewindow.hideLastInterval && timewindow.hideQuickInterval"
formControlName="timewindowMs"
predefinedName="timewindow.last"
required
style="padding-top: 8px;"></tb-timeinterval>
<tb-quick-time-interval
*ngIf="quickIntervalOnly || !isEdit && timewindow.hideLastInterval && !timewindow.hideQuickInterval"
formControlName="quickInterval"
onlyCurrentInterval="true"
required
style="padding-top: 8px"></tb-quick-time-interval>
<form [formGroup]="timewindowForm" class="tb-timewindow-form">
<div class="tb-timewindow-form-header tb-form-panel no-border no-padding-bottom no-gap" *ngIf="!historyOnly">
<tb-toggle-select class="tb-timewindow-form-type-options" appearance="fill"
[options]="timewindowTypeOptions" formControlName="selectedTab">
</tb-toggle-select>
<button mat-icon-button type="button" class="tb-timewindow-form-settings-btn tb-mat-24"
*ngIf="isEdit"
(click)="openTimewindowConfig()">
<mat-icon>settings</mat-icon>
</button>
</div>
<div class="tb-timewindow-form-content tb-form-panel no-border">
<ng-container *ngIf="timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME">
<section class="tb-form-panel stroked" *ngIf="realtimeIntervalSelectionAvailable; else timezoneSelectionPanel">
<div class="tb-flex space-between"
[ngClass]="{'align-end': realtimeTypeSelectionAvailable, 'align-center': !realtimeTypeSelectionAvailable }">
<div class="tb-flex-xs column">
<div class="tb-form-panel-title">{{ 'timewindow.timewindow' | translate }}</div>
<ng-container formGroupName="realtime" *ngIf="realtimeTypeSelectionAvailable">
<tb-toggle-select appearance="stroked" [options]="realtimeTimewindowOptions" formControlName="realtimeType">
</tb-toggle-select>
</ng-container>
</div>
</section>
<ng-container *ngTemplateOutlet="timezoneSelection">
</ng-container>
</div>
<div class="tb-form-row no-border no-padding" formGroupName="realtime">
<tb-timeinterval
*ngIf="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
formControlName="timewindowMs"
subscriptSizing="dynamic"
appearance="outline"
[disabledAdvanced]="timewindow.realtime.disableCustomInterval"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL">
</tb-timeinterval>
<tb-quick-time-interval
*ngIf="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.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>
<ng-container *ngTemplateOutlet="additionalData">
</ng-container>
</mat-tab>
<mat-tab label="{{ 'timewindow.history' | translate }}">
<section fxLayout="row">
<section *ngIf="isEdit" fxLayout="column" fxLayoutAlign="start center"
style="padding-top: 8px; padding-left: 16px;">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideInterval"
(ngModelChange)="onHideIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval">
<div formGroupName="history" class="mat-content mat-padding" style="padding-top: 8px;">
<mat-radio-group formControlName="historyType">
<mat-radio-button *ngIf="forAllTimeEnabled" [value]="historyTypes.FOR_ALL_TIME" color="primary">
<section fxLayout="column">
<span translate>timewindow.for-all-time</span>
</section>
</mat-radio-button>
<mat-radio-button [value]="historyTypes.LAST_INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.last</span>
<tb-timeinterval
formControlName="timewindowMs"
predefinedName="timewindow.last"
class="history-time-input"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
style="padding-top: 8px;"></tb-timeinterval>
</section>
</mat-radio-button>
<mat-radio-button [value]="historyTypes.FIXED" color="primary">
<section fxLayout="column">
<span translate>timewindow.time-period</span>
<tb-datetime-period
formControlName="fixedTimewindow"
class="history-time-input"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.FIXED"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.FIXED"
style="padding-top: 8px;"></tb-datetime-period>
</section>
</mat-radio-button>
<mat-radio-button [value]="historyTypes.INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
class="history-time-input"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
style="padding-top: 8px"></tb-quick-time-interval>
</section>
</mat-radio-button>
</mat-radio-group>
</ng-container>
<ng-container *ngIf="timewindowForm.get('selectedTab').value === timewindowTypes.HISTORY">
<section class="tb-form-panel stroked" *ngIf="historyIntervalSelectionAvailable; else timezoneSelectionPanel">
<div class="tb-flex space-between"
[ngClass]="{'align-end': historyTypeSelectionAvailable, 'align-center': !historyTypeSelectionAvailable }">
<div class="tb-flex-xs column">
<div class="tb-form-panel-title">{{ 'timewindow.timewindow' | translate }}</div>
<ng-container formGroupName="history" *ngIf="historyTypeSelectionAvailable">
<tb-toggle-select appearance="stroked" [options]="historyTimewindowOptions" formControlName="historyType">
</tb-toggle-select>
</ng-container>
</div>
</section>
<ng-container *ngTemplateOutlet="timezoneSelection">
</ng-container>
</div>
<div class="tb-form-row no-border no-padding"
formGroupName="history" *ngIf="historyIntervalSelectionAvailable &&
timewindowForm.get('history.historyType').value !== historyTypes.FOR_ALL_TIME">
<tb-timeinterval
*ngIf="timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
formControlName="timewindowMs"
subscriptSizing="dynamic"
appearance="outline"
[disabledAdvanced]="timewindow.history.disableCustomInterval"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL">
</tb-timeinterval>
<tb-datetime-period
*ngIf="timewindowForm.get('history.historyType').value === historyTypes.FIXED"
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>
<tb-quick-time-interval
*ngIf="timewindowForm.get('history.historyType').value === historyTypes.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 *ngTemplateOutlet="additionalData">
</ng-container>
</mat-tab>
</mat-tab-group>
<ng-template #additionalData>
<div *ngIf="aggregation" formGroupName="aggregation" class="mat-content mat-padding" fxLayout="column">
<section fxLayout="row">
<section fxLayout="column" fxLayoutAlign="start center" [fxShow]="isEdit">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggregation"
(ngModelChange)="onHideAggregationChanged()"></mat-checkbox>
</section>
<section fxFlex fxLayout="column" [fxShow]="isEdit || !timewindow.hideAggregation">
<mat-form-field>
<mat-label translate>aggregation.function</mat-label>
<mat-select formControlName="type" style="min-width: 150px;">
</ng-container>
<ng-container *ngIf="aggregationOptionsAvailable">
<ng-container formGroupName="aggregation">
<section class="tb-form-row column-xs space-between same-padding" *ngIf="isEdit || !timewindow.hideAggregation">
<div class="fixed-title-width-180">{{ 'aggregation.aggregation' | translate }}</div>
<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>
</section>
</section>
<section fxLayout="row" *ngIf="timewindowForm.get('aggregation.type').value === aggregationTypes.NONE">
<section fxLayout="column" fxLayoutAlign="start center" [fxShow]="isEdit">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggInterval"
(ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval">
<div class="limit-slider-container" fxLayout="row" fxLayoutAlign="start center"
fxLayout.xs="column" fxLayoutAlign.xs="stretch">
<label translate>aggregation.limit</label>
<div fxLayout="row" fxLayoutAlign="start center" fxFlex>
<mat-slider fxFlex
discrete
min="{{minDatapointsLimit()}}"
max="{{maxDatapointsLimit()}}"><input matSliderThumb formControlName="limit"/>
</mat-slider>
<mat-form-field class="limit-slider-value">
<input matInput formControlName="limit" type="number" step="1"
[value]="timewindowForm.get('aggregation.limit').value"
min="{{minDatapointsLimit()}}"
max="{{maxDatapointsLimit()}}"/>
</mat-form-field>
</div>
</div>
<section class="tb-form-row column-xs space-between same-padding"
*ngIf="timewindowForm.get('aggregation.type').value === aggregationTypes.NONE && (isEdit || !timewindow.hideAggInterval)">
<div>{{ 'aggregation.limit' | translate }}</div>
<tb-datapoints-limit formControlName="limit"
[required]="timewindowForm.get('aggregation.type').value === aggregationTypes.NONE">
</tb-datapoints-limit>
</section>
</ng-container>
<section class="tb-form-row column-xs same-padding" [fxShow]="(isEdit || !timewindow.hideAggInterval)
&& timewindowForm.get('aggregation.type').value !== aggregationTypes.NONE">
<ng-container formGroupName="realtime" *ngIf="timewindow.selectedTab === timewindowTypes.REALTIME">
<tb-timeinterval
formControlName="interval"
[min]="minRealtimeAggInterval()" [max]="maxRealtimeAggInterval()"
useCalendarIntervals
subscriptSizing="dynamic"
appearance="outline"
[disabledAdvanced]="timewindow.realtime.disableCustomGroupInterval">
<div class="fixed-title-width-180">{{ 'aggregation.group-interval' | translate }}</div>
</tb-timeinterval>
</ng-container>
<ng-container formGroupName="history" *ngIf="timewindow.selectedTab === timewindowTypes.HISTORY">
<tb-timeinterval
formControlName="interval"
[min]="minHistoryAggInterval()" [max]="maxHistoryAggInterval()"
useCalendarIntervals
subscriptSizing="dynamic"
appearance="outline"
[disabledAdvanced]="timewindow.history.disableCustomGroupInterval">
<div class="fixed-title-width-180">{{ 'aggregation.group-interval' | translate }}</div>
</tb-timeinterval>
</ng-container>
</section>
</div>
<div formGroupName="realtime"
*ngIf="aggregation && timewindowForm.get('aggregation.type').value !== aggregationTypes.NONE &&
timewindow.selectedTab === timewindowTypes.REALTIME" class="mat-content mat-padding" fxLayout="column">
<tb-timeinterval
formControlName="interval"
[isEdit]="isEdit"
[(hideFlag)]="timewindow.hideAggInterval"
(hideFlagChange)="onHideAggIntervalChanged()"
[min]="minRealtimeAggInterval()" [max]="maxRealtimeAggInterval()"
useCalendarIntervals
predefinedName="aggregation.group-interval">
</tb-timeinterval>
</div>
<div formGroupName="history"
*ngIf="aggregation && timewindowForm.get('aggregation.type').value !== aggregationTypes.NONE &&
timewindow.selectedTab === timewindowTypes.HISTORY" class="mat-content mat-padding" fxLayout="column">
<tb-timeinterval
formControlName="interval"
[isEdit]="isEdit"
[(hideFlag)]="timewindow.hideAggInterval"
(hideFlagChange)="onHideAggIntervalChanged()"
[min]="minHistoryAggInterval()" [max]="maxHistoryAggInterval()"
useCalendarIntervals
predefinedName="aggregation.group-interval">
</tb-timeinterval>
</div>
<div *ngIf="timezone" class="mat-content mat-padding" fxLayout="row">
<section fxLayout="column" fxLayoutAlign="start center" [fxShow]="isEdit">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideTimezone"
(ngModelChange)="onHideTimezoneChanged()"></mat-checkbox>
</section>
<tb-timezone-select fxFlex [fxShow]="isEdit || !timewindow.hideTimezone"
localBrowserTimezonePlaceholderOnEmpty="true"
formControlName="timezone">
</tb-timezone-select>
</div>
</ng-container>
</div>
<ng-template #timezoneSelectionPanel>
<section class="tb-form-row space-between same-padding">
<div>{{ 'timezone.timezone' | translate }}</div>
<ng-container *ngTemplateOutlet="timezoneSelection">
</ng-container>
</section>
</ng-template>
<ng-template #timezoneSelection>
<tb-timezone *ngIf="timezone && (isEdit || !timewindow.hideTimezone)"
asButton strokedButton noMargin
localBrowserTimezonePlaceholderOnEmpty="true"
formControlName="timezone">
</tb-timezone>
</ng-template>
<mat-divider></mat-divider>
<div class="tb-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) || timewindowForm.invalid || !timewindowForm.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
<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.update' | translate }}
</button>
</div>

67
ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss

@ -13,69 +13,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../scss/constants";
:host {
display: flex;
flex-direction: column;
max-height: 100%;
max-width: 100%;
background-color: #fff;
width: 450px;
.mat-content {
overflow: hidden;
}
.mat-padding {
padding: 0 16px;
}
.hide-label {
margin-bottom: 5px;
margin-right: 5px;
}
tb-timeinterval[ng-reflect-fx-show="true"] {
margin-bottom: -16px;
}
.tb-timewindow-form {
display: flex;
flex-direction: column;
max-height: 100%;
.limit-slider-container {
.limit-slider-value {
margin-left: 16px;
min-width: 25px;
max-width: 100px;
&-header {
flex-direction: row;
align-items: center;
}
mat-form-field input[type=number] {
text-align: center;
}
}
@media #{$mat-gt-sm} {
.history-time-input {
min-width: 364px;
&-type-options {
flex: 1;
}
.limit-slider-container {
> label {
margin-right: 16px;
width: min-content;
max-width: 40%;
}
}
}
}
:host ::ng-deep {
.mat-mdc-radio-button {
display: block;
.mdc-form-field {
align-items: start;
> label {
padding-top: 10px;
}
&-settings-btn {
color: rgba(0, 0, 0, 0.54);
}
}
.mat-mdc-tab-group:not(.tb-headless) {
height: 100%;
}
}

423
ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

@ -14,14 +14,16 @@
/// limitations under the License.
///
import { Component, Inject, InjectionToken, OnInit, ViewContainerRef } from '@angular/core';
import { Component, Inject, InjectionToken, OnDestroy, OnInit, ViewContainerRef } from '@angular/core';
import {
aggregationTranslations,
AggregationType,
DAY,
HistoryWindowType,
historyWindowTypeTranslations,
quickTimeIntervalPeriod,
RealtimeWindowType,
realtimeWindowTypeTranslations,
Timewindow,
TimewindowType
} from '@shared/models/time/time.models';
@ -30,8 +32,17 @@ 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 { isDefined } from '@core/utils';
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;
@ -48,9 +59,9 @@ export const TIMEWINDOW_PANEL_DATA = new InjectionToken<any>('TimewindowPanelDat
@Component({
selector: 'tb-timewindow-panel',
templateUrl: './timewindow-panel.component.html',
styleUrls: ['./timewindow-panel.component.scss']
styleUrls: ['./timewindow-panel.component.scss', './timewindow-form.scss']
})
export class TimewindowPanelComponent extends PageComponent implements OnInit {
export class TimewindowPanelComponent extends PageComponent implements OnInit, OnDestroy {
historyOnly = false;
@ -82,12 +93,31 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
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;
private destroy$ = new Subject<void>();
constructor(@Inject(TIMEWINDOW_PANEL_DATA) public data: TimewindowPanelData,
public overlayRef: OverlayRef,
protected store: Store<AppState>,
public fb: UntypedFormBuilder,
private timeService: TimeService,
public viewContainerRef: ViewContainerRef) {
private translate: TranslateService,
public viewContainerRef: ViewContainerRef,
private dialog: MatDialog) {
super(store);
this.historyOnly = data.historyOnly;
this.forAllTimeEnabled = data.forAllTimeEnabled;
@ -96,12 +126,68 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
this.aggregation = data.aggregation;
this.timezone = data.timezone;
this.isEdit = data.isEdit;
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 hideInterval = this.timewindow.hideInterval || false;
const hideLastInterval = this.timewindow.hideLastInterval || false;
const hideQuickInterval = this.timewindow.hideQuickInterval || false;
const hideAggregation = this.timewindow.hideAggregation || false;
const hideAggInterval = this.timewindow.hideAggInterval || false;
const hideTimezone = this.timewindow.hideTimezone || false;
@ -110,49 +196,86 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
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) ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL,
disabled: hideInterval
value: isDefined(realtime?.realtimeType) ? realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL,
disabled: realtime.hideInterval
}],
timewindowMs: [{
value: isDefined(realtime?.timewindowMs) ? this.timewindow.realtime.timewindowMs : null,
disabled: hideInterval || hideLastInterval
value: isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : null,
disabled: realtime.hideInterval || realtime.hideLastInterval
}],
interval: [{
value:isDefined(realtime?.interval) ? realtime.interval : null,
disabled: hideAggInterval
}],
interval: [isDefined(realtime?.interval) ? this.timewindow.realtime.interval : null],
quickInterval: [{
value: isDefined(realtime?.quickInterval) ? this.timewindow.realtime.quickInterval : null,
disabled: hideInterval || hideQuickInterval
value: isDefined(realtime?.quickInterval) ? realtime.quickInterval : null,
disabled: realtime.hideInterval || realtime.hideQuickInterval
}]
}),
history: this.fb.group({
historyType: [{
value: isDefined(history?.historyType) ? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL,
disabled: hideInterval
value: isDefined(history?.historyType) ? history.historyType : HistoryWindowType.LAST_INTERVAL,
disabled: history.hideInterval
}],
timewindowMs: [{
value: isDefined(history?.timewindowMs) ? this.timewindow.history.timewindowMs : null,
disabled: hideInterval
value: isDefined(history?.timewindowMs) ? history.timewindowMs : null,
disabled: history.hideInterval || history.hideLastInterval
}],
interval: [{
value:isDefined(history?.interval) ? history.interval : null,
disabled: hideAggInterval
}],
interval: [ isDefined(history?.interval) ? this.timewindow.history.interval : null
],
fixedTimewindow: [{
value: isDefined(history?.fixedTimewindow) ? this.timewindow.history.fixedTimewindow : null,
disabled: hideInterval
value: isDefined(history?.fixedTimewindow) ? history.fixedTimewindow : null,
disabled: history.hideInterval || history.hideFixedInterval
}],
quickInterval: [{
value: isDefined(history?.quickInterval) ? this.timewindow.history.quickInterval : null,
disabled: hideInterval
value: isDefined(history?.quickInterval) ? history.quickInterval : null,
disabled: history.hideInterval || history.hideQuickInterval
}]
}),
aggregation: this.fb.group({
type: [{
value: isDefined(aggregation?.type) ? this.timewindow.aggregation.type : null,
value: isDefined(aggregation?.type) ? aggregation.type : null,
disabled: hideAggregation
}],
limit: [{
value: isDefined(aggregation?.limit) ? this.checkLimit(this.timewindow.aggregation.limit) : null,
value: isDefined(aggregation?.limit) ? aggregation.limit : null,
disabled: hideAggInterval
}, []]
}),
@ -162,35 +285,41 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
}]
});
this.updateValidators(this.timewindowForm.get('aggregation.type').value);
this.timewindowForm.get('aggregation.type').valueChanges.subscribe((aggregationType: AggregationType) => {
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);
});
}
private checkLimit(limit?: number): number {
if (!limit || limit < this.minDatapointsLimit()) {
return this.minDatapointsLimit();
} else if (limit > this.maxDatapointsLimit()) {
return this.maxDatapointsLimit();
}
return limit;
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.min(this.minDatapointsLimit()),
Validators.max(this.maxDatapointsLimit())]);
this.timewindowForm.get('aggregation.limit').setValidators([Validators.required]);
}
this.timewindowForm.get('aggregation.limit').updateValueAndValidity({emitEvent: false});
}
onTimewindowTypeChange() {
this.timewindowForm.markAsDirty();
private onTimewindowTypeChange(selectedTab: TimewindowType) {
const timewindowFormValue = this.timewindowForm.getRawValue();
if (this.timewindow.selectedTab === TimewindowType.REALTIME) {
if (timewindowFormValue.history.historyType !== HistoryWindowType.FIXED) {
if (selectedTab === TimewindowType.REALTIME) {
if (timewindowFormValue.history.historyType !== HistoryWindowType.FIXED
&& !((this.quickIntervalOnly || this.timewindow.realtime.hideLastInterval)
&& timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL)
&& !(this.timewindow.realtime.hideQuickInterval && timewindowFormValue.history.historyType === HistoryWindowType.INTERVAL)) {
this.timewindowForm.get('realtime').patchValue({
realtimeType: Object.keys(RealtimeWindowType).includes(HistoryWindowType[timewindowFormValue.history.historyType]) ?
RealtimeWindowType[HistoryWindowType[timewindowFormValue.history.historyType]] :
@ -201,7 +330,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
});
setTimeout(() => this.timewindowForm.get('realtime.interval').patchValue(timewindowFormValue.history.interval));
}
} else {
} else if (!(this.timewindow.history.hideLastInterval && timewindowFormValue.realtime.realtimeType === RealtimeWindowType.LAST_INTERVAL)
&& !(this.timewindow.history.hideQuickInterval && timewindowFormValue.realtime.realtimeType === RealtimeWindowType.INTERVAL)) {
this.timewindowForm.get('history').patchValue({
historyType: HistoryWindowType[RealtimeWindowType[timewindowFormValue.realtime.realtimeType]],
timewindowMs: timewindowFormValue.realtime.timewindowMs,
@ -219,20 +350,27 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
}
update() {
this.prepareTimewindowConfig();
this.result = this.timewindow;
this.overlayRef.dispose();
}
private prepareTimewindowConfig() {
const timewindowFormValue = this.timewindowForm.getRawValue();
this.timewindow.realtime = {
realtimeType: timewindowFormValue.realtime.realtimeType,
timewindowMs: timewindowFormValue.realtime.timewindowMs,
quickInterval: timewindowFormValue.realtime.quickInterval,
interval: timewindowFormValue.realtime.interval
};
this.timewindow.history = {
historyType: timewindowFormValue.history.historyType,
timewindowMs: timewindowFormValue.history.timewindowMs,
interval: timewindowFormValue.history.interval,
fixedTimewindow: timewindowFormValue.history.fixedTimewindow,
quickInterval: timewindowFormValue.history.quickInterval,
};
this.timewindow.selectedTab = timewindowFormValue.selectedTab;
this.timewindow.realtime = {...this.timewindow.realtime, ...{
realtimeType: timewindowFormValue.realtime.realtimeType,
timewindowMs: timewindowFormValue.realtime.timewindowMs,
quickInterval: timewindowFormValue.realtime.quickInterval,
interval: timewindowFormValue.realtime.interval
}};
this.timewindow.history = {...this.timewindow.history, ...{
historyType: timewindowFormValue.history.historyType,
timewindowMs: timewindowFormValue.history.timewindowMs,
interval: timewindowFormValue.history.interval,
fixedTimewindow: timewindowFormValue.history.fixedTimewindow,
quickInterval: timewindowFormValue.history.quickInterval,
}};
if (this.aggregation) {
this.timewindow.aggregation = {
type: timewindowFormValue.aggregation.type,
@ -242,20 +380,79 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
if (this.timezone) {
this.timewindow.timezone = timewindowFormValue.timezone;
}
this.result = this.timewindow;
this.overlayRef.dispose();
}
cancel() {
this.overlayRef.dispose();
}
private updateTimewindowForm() {
this.timewindowForm.patchValue(this.timewindow);
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});
}
}
minDatapointsLimit() {
return this.timeService.getMinDatapointsLimit();
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();
}
maxDatapointsLimit() {
return this.timeService.getMaxDatapointsLimit();
cancel() {
this.overlayRef.dispose();
}
minRealtimeAggInterval() {
@ -300,84 +497,24 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
}
}
onHideIntervalChanged() {
if (this.timewindow.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});
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('history.historyType').enable({emitEvent: false});
this.timewindowForm.get('history.timewindowMs').enable({emitEvent: false});
this.timewindowForm.get('history.fixedTimewindow').enable({emitEvent: false});
this.timewindowForm.get('history.quickInterval').enable({emitEvent: false});
this.timewindowForm.get('realtime.realtimeType').enable({emitEvent: false});
if (!this.timewindow.hideLastInterval) {
this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false});
}
if (!this.timewindow.hideQuickInterval) {
this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false});
}
}
this.timewindowForm.markAsDirty();
}
onHideLastIntervalChanged() {
if (this.timewindow.hideLastInterval) {
this.timewindowForm.get('realtime.timewindowMs').disable({emitEvent: false});
if (!this.timewindow.hideQuickInterval) {
this.timewindowForm.get('realtime.realtimeType').setValue(RealtimeWindowType.INTERVAL);
}
} else {
if (!this.timewindow.hideInterval) {
this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false});
}
}
this.timewindowForm.markAsDirty();
}
onHideQuickIntervalChanged() {
if (this.timewindow.hideQuickInterval) {
this.timewindowForm.get('realtime.quickInterval').disable({emitEvent: false});
if (!this.timewindow.hideLastInterval) {
this.timewindowForm.get('realtime.realtimeType').setValue(RealtimeWindowType.LAST_INTERVAL);
}
} else {
if (!this.timewindow.hideInterval) {
this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false});
}
}
this.timewindowForm.markAsDirty();
}
onHideAggregationChanged() {
if (this.timewindow.hideAggregation) {
this.timewindowForm.get('aggregation.type').disable({emitEvent: false});
} else {
this.timewindowForm.get('aggregation.type').enable({emitEvent: false});
}
this.timewindowForm.markAsDirty();
}
onHideAggIntervalChanged() {
if (this.timewindow.hideAggInterval) {
this.timewindowForm.get('aggregation.limit').disable({emitEvent: false});
} else {
this.timewindowForm.get('aggregation.limit').enable({emitEvent: false});
}
this.timewindowForm.markAsDirty();
}
onHideTimezoneChanged() {
if (this.timewindow.hideTimezone) {
this.timewindowForm.get('timezone').disable({emitEvent: false});
} else {
this.timewindowForm.get('timezone').enable({emitEvent: false});
}
this.timewindowForm.markAsDirty();
openTimewindowConfig() {
this.prepareTimewindowConfig();
this.dialog.open<TimewindowConfigDialogComponent, TimewindowConfigDialogData, Timewindow>(
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.updateTimewindowForm();
}
});
}
}

1
ui-ngx/src/app/shared/components/time/timewindow.component.html

@ -44,6 +44,7 @@
[class]="{'no-padding': noPadding}"
matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition"
[matTooltipDisabled]="timewindowDisabled"
[style]="timewindowComponentStyle"
(click)="toggleTimewindow($event)">
<tb-icon *ngIf="computedTimewindowStyle.showIcon && computedTimewindowStyle.iconPosition === 'left'"

14
ui-ngx/src/app/shared/components/time/timewindow.component.ts

@ -220,14 +220,14 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan
if ($event) {
$event.stopPropagation();
}
if (this.disablePanel) {
if (this.disablePanel || this.timewindowDisabled) {
return;
}
const config = new OverlayConfig({
panelClass: 'tb-timewindow-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
maxHeight: '80vh',
maxHeight: '70vh',
height: 'min-content'
});
@ -363,8 +363,14 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan
private isTimewindowDisabled(): boolean {
return this.disabled ||
(!this.isEdit && (!this.innerValue || this.innerValue.hideInterval &&
(!this.aggregation || this.innerValue.hideAggregation && this.innerValue.hideAggInterval)));
(!this.isEdit && (!this.innerValue || (
((this.innerValue.realtime?.hideInterval && this.innerValue.history?.hideInterval) ||
(this.innerValue.realtime?.hideLastInterval && this.innerValue.realtime?.hideQuickInterval &&
this.innerValue.history?.hideLastInterval && this.innerValue.history?.hideFixedInterval &&
this.innerValue.history?.hideQuickInterval)) &&
(!this.aggregation || this.innerValue.hideAggregation && this.innerValue.hideAggInterval) &&
(!this.timezone || this.innerValue.hideTimezone)
)));
}
}

43
ui-ngx/src/app/shared/components/time/timezone-panel.component.html

@ -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>

33
ui-ngx/src/app/shared/components/time/timezone-panel.component.scss

@ -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;
}
}

80
ui-ngx/src/app/shared/components/time/timezone-panel.component.ts

@ -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);
}
}
}

5
ui-ngx/src/app/shared/components/time/timezone-select.component.html

@ -15,8 +15,9 @@
limitations under the License.
-->
<mat-form-field [formGroup]="selectTimezoneFormGroup" fxFlex class="mat-block" [appearance]="appearance">
<mat-label translate>timezone.timezone</mat-label>
<mat-form-field [formGroup]="selectTimezoneFormGroup" fxFlex class="flex mat-block"
[subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label *ngIf="displayLabel" translate>timezone.timezone</mat-label>
<input matInput type="text" placeholder="{{ 'timezone.select-timezone' | translate }}"
#timezoneInput
formControlName="timezone"

38
ui-ngx/src/app/shared/components/time/timezone-select.component.ts

@ -15,8 +15,8 @@
///
import { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatFormFieldAppearance } from '@angular/material/form-field';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { Observable, of } from 'rxjs';
import { map, mergeMap, share, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
@ -26,6 +26,8 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { getDefaultTimezoneInfo, getTimezoneInfo, getTimezones, TimezoneInfo } from '@shared/models/time/time.models';
import { deepClone } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TimeService } from '@core/services/time.service';
@Component({
selector: 'tb-timezone-select',
@ -45,9 +47,6 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
defaultTimezoneId: string = null;
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
set defaultTimezone(timezone: string) {
if (this.defaultTimezoneId !== timezone) {
@ -55,6 +54,16 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
}
}
@Input()
@coerceBoolean()
displayLabel = true;
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
@ -95,7 +104,7 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
private dirty = false;
private localBrowserTimezoneInfoPlaceholder: TimezoneInfo;
private localBrowserTimezoneInfoPlaceholder: TimezoneInfo = this.timeService.getLocalBrowserTimezoneInfoPlaceholder();
private timezones: Array<TimezoneInfo>;
@ -104,7 +113,8 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
constructor(private store: Store<AppState>,
public translate: TranslateService,
private ngZone: NgZone,
private fb: UntypedFormBuilder) {
private fb: UntypedFormBuilder,
private timeService: TimeService) {
this.selectTimezoneFormGroup = this.fb.group({
timezone: [null]
});
@ -165,7 +175,7 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
} else {
this.modelValue = null;
if (this.localBrowserTimezonePlaceholderOnEmptyValue) {
this.selectTimezoneFormGroup.get('timezone').patchValue(this.getLocalBrowserTimezoneInfoPlaceholder(), {emitEvent: false});
this.selectTimezoneFormGroup.get('timezone').patchValue(this.localBrowserTimezoneInfoPlaceholder, {emitEvent: false});
} else {
this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false});
}
@ -194,7 +204,7 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
}
} else if (this.localBrowserTimezonePlaceholderOnEmptyValue) {
this.ngZone.run(() => {
this.selectTimezoneFormGroup.get('timezone').reset(this.getLocalBrowserTimezoneInfoPlaceholder(), {emitEvent: true});
this.selectTimezoneFormGroup.get('timezone').reset(this.localBrowserTimezoneInfoPlaceholder, {emitEvent: true});
});
}
}
@ -232,19 +242,11 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
if (!this.timezones) {
this.timezones = [];
if (this.localBrowserTimezonePlaceholderOnEmptyValue) {
this.timezones.push(this.getLocalBrowserTimezoneInfoPlaceholder());
this.timezones.push(this.localBrowserTimezoneInfoPlaceholder);
}
this.timezones.push(...getTimezones());
}
return this.timezones;
}
private getLocalBrowserTimezoneInfoPlaceholder(): TimezoneInfo {
if (!this.localBrowserTimezoneInfoPlaceholder) {
this.localBrowserTimezoneInfoPlaceholder = deepClone(getDefaultTimezoneInfo());
this.localBrowserTimezoneInfoPlaceholder.id = null;
this.localBrowserTimezoneInfoPlaceholder.name = this.translate.instant('timezone.browser-time');
}
return this.localBrowserTimezoneInfoPlaceholder;
}
}

54
ui-ngx/src/app/shared/components/time/timezone.component.html

@ -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>

64
ui-ngx/src/app/shared/components/time/timezone.component.scss

@ -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;
}
}
}

235
ui-ngx/src/app/shared/components/time/timezone.component.ts

@ -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;
}
}
}
}

2
ui-ngx/src/app/shared/models/overlay.models.ts

@ -41,4 +41,4 @@ export const POSITION_MAP: { [key: string]: ConnectionPositionPair } = {
};
export const DEFAULT_OVERLAY_POSITIONS = [POSITION_MAP.bottomLeft, POSITION_MAP.bottomRight, POSITION_MAP.topLeft,
POSITION_MAP.topRight, POSITION_MAP.left, POSITION_MAP.right];
POSITION_MAP.topRight, POSITION_MAP.left, POSITION_MAP.right, POSITION_MAP.bottom];

3
ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts

@ -186,7 +186,8 @@ export enum IntervalType {
WEEK = 'WEEK',
WEEK_ISO = 'WEEK_ISO',
MONTH = 'MONTH',
QUARTER = 'QUARTER'
QUARTER = 'QUARTER',
CUSTOM = 'CUSTOM'
}
export class TimeseriesSubscriptionCmd extends SubscriptionCmd {

96
ui-ngx/src/app/shared/models/time/time.models.ts

@ -15,7 +15,7 @@
///
import { TimeService } from '@core/services/time.service';
import { deepClone, isDefined, isNumeric, isUndefined } from '@app/core/utils';
import { deepClone, isDefined, isDefinedAndNotNull, isNumeric, isUndefined } from '@app/core/utils';
import * as moment_ from 'moment';
import * as momentTz from 'moment-timezone';
import { IntervalType } from '@shared/models/telemetry/telemetry.models';
@ -53,6 +53,17 @@ export enum HistoryWindowType {
FOR_ALL_TIME
}
export const realtimeWindowTypeTranslations = new Map<RealtimeWindowType, string>([
[RealtimeWindowType.LAST_INTERVAL, 'timewindow.last'],
[RealtimeWindowType.INTERVAL, 'timewindow.relative']
]);
export const historyWindowTypeTranslations = new Map<HistoryWindowType, string>([
[HistoryWindowType.LAST_INTERVAL, 'timewindow.last'],
[HistoryWindowType.FIXED, 'timewindow.range'],
[HistoryWindowType.INTERVAL, 'timewindow.relative'],
[HistoryWindowType.FOR_ALL_TIME, 'timewindow.for-all-time']
]);
export type Interval = number | IntervalType;
export class IntervalMath {
@ -77,6 +88,12 @@ export interface IntervalWindow {
interval?: Interval;
timewindowMs?: number;
quickInterval?: QuickTimeInterval;
disableCustomInterval?: boolean;
disableCustomGroupInterval?: boolean;
hideInterval?: boolean;
hideLastInterval?: boolean;
hideQuickInterval?: boolean;
hideFixedInterval?: boolean;
}
export interface RealtimeWindow extends IntervalWindow{
@ -122,9 +139,6 @@ export interface Aggregation {
export interface Timewindow {
displayValue?: string;
displayTimezoneAbbr?: string;
hideInterval?: boolean;
hideQuickInterval?: boolean;
hideLastInterval?: boolean;
hideAggregation?: boolean;
hideAggInterval?: boolean;
hideTimezone?: boolean;
@ -241,9 +255,6 @@ export const defaultTimewindow = (timeService: TimeService): Timewindow => {
const currentTime = moment().valueOf();
return {
displayValue: '',
hideInterval: false,
hideLastInterval: false,
hideQuickInterval: false,
hideAggregation: false,
hideAggInterval: false,
hideTimezone: false,
@ -252,7 +263,10 @@ export const defaultTimewindow = (timeService: TimeService): Timewindow => {
realtimeType: RealtimeWindowType.LAST_INTERVAL,
interval: SECOND,
timewindowMs: MINUTE,
quickInterval: QuickTimeInterval.CURRENT_DAY
quickInterval: QuickTimeInterval.CURRENT_DAY,
hideInterval: false,
hideLastInterval: false,
hideQuickInterval: false
},
history: {
historyType: HistoryWindowType.LAST_INTERVAL,
@ -262,7 +276,11 @@ export const defaultTimewindow = (timeService: TimeService): Timewindow => {
startTimeMs: currentTime - DAY,
endTimeMs: currentTime
},
quickInterval: QuickTimeInterval.CURRENT_DAY
quickInterval: QuickTimeInterval.CURRENT_DAY,
hideInterval: false,
hideLastInterval: false,
hideFixedInterval: false,
hideQuickInterval: false
},
aggregation: {
type: AggregationType.AVG,
@ -283,14 +301,37 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO
historyOnly: boolean, timeService: TimeService): Timewindow => {
const model = defaultTimewindow(timeService);
if (value) {
model.hideInterval = value.hideInterval;
model.hideLastInterval = value.hideLastInterval;
model.hideQuickInterval = value.hideQuickInterval;
model.hideAggregation = value.hideAggregation;
model.hideAggInterval = value.hideAggInterval;
model.hideTimezone = value.hideTimezone;
model.selectedTab = getTimewindowType(value);
// for backward compatibility
if (isDefinedAndNotNull((value as any).hideInterval)) {
model.realtime.hideInterval = (value as any).hideInterval;
model.history.hideInterval = (value as any).hideInterval;
delete (value as any).hideInterval;
}
if (isDefinedAndNotNull((value as any).hideLastInterval)) {
model.realtime.hideLastInterval = (value as any).hideLastInterval;
delete (value as any).hideLastInterval;
}
if (isDefinedAndNotNull((value as any).hideQuickInterval)) {
model.realtime.hideQuickInterval = (value as any).hideQuickInterval;
delete (value as any).hideQuickInterval;
}
if (isDefined(value.realtime)) {
if (isDefinedAndNotNull(value.realtime.hideInterval)) {
model.realtime.hideInterval = value.realtime.hideInterval;
}
if (isDefinedAndNotNull(value.realtime.hideLastInterval)) {
model.realtime.hideLastInterval = value.realtime.hideLastInterval;
}
if (isDefinedAndNotNull(value.realtime.hideQuickInterval)) {
model.realtime.hideQuickInterval = value.realtime.hideQuickInterval;
}
if (isDefined(value.realtime.interval)) {
model.realtime.interval = value.realtime.interval;
}
@ -311,6 +352,19 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO
}
}
if (isDefined(value.history)) {
if (isDefinedAndNotNull(value.history.hideInterval)) {
model.history.hideInterval = value.history.hideInterval;
}
if (isDefinedAndNotNull(value.history.hideLastInterval)) {
model.history.hideLastInterval = value.history.hideLastInterval;
}
if (isDefinedAndNotNull(value.history.hideFixedInterval)) {
model.history.hideFixedInterval = value.history.hideFixedInterval;
}
if (isDefinedAndNotNull(value.history.hideQuickInterval)) {
model.history.hideQuickInterval = value.history.hideQuickInterval;
}
if (isDefined(value.history.interval)) {
model.history.interval = value.history.interval;
}
@ -376,9 +430,6 @@ export const toHistoryTimewindow = (timewindow: Timewindow, startTimeMs: number,
limit = timeService.getMaxDatapointsLimit();
}
return {
hideInterval: timewindow.hideInterval || false,
hideLastInterval: timewindow.hideLastInterval || false,
hideQuickInterval: timewindow.hideQuickInterval || false,
hideAggregation: timewindow.hideAggregation || false,
hideAggInterval: timewindow.hideAggInterval || false,
hideTimezone: timewindow.hideTimezone || false,
@ -389,7 +440,10 @@ export const toHistoryTimewindow = (timewindow: Timewindow, startTimeMs: number,
startTimeMs,
endTimeMs
},
interval: timeService.boundIntervalToTimewindow(endTimeMs - startTimeMs, interval, AggregationType.AVG)
interval: timeService.boundIntervalToTimewindow(endTimeMs - startTimeMs, interval, AggregationType.AVG),
hideInterval: timewindow.history?.hideInterval || false,
hideLastInterval: timewindow.history?.hideLastInterval || false,
hideQuickInterval: timewindow.history?.hideQuickInterval || false,
},
aggregation: {
type: aggType,
@ -844,20 +898,14 @@ export const createTimewindowForComparison = (subscriptionTimewindow: Subscripti
export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => {
const cloned: Timewindow = {};
cloned.hideInterval = timewindow.hideInterval || false;
cloned.hideLastInterval = timewindow.hideLastInterval || false;
cloned.hideQuickInterval = timewindow.hideQuickInterval || false;
cloned.hideAggregation = timewindow.hideAggregation || false;
cloned.hideAggInterval = timewindow.hideAggInterval || false;
cloned.hideTimezone = timewindow.hideTimezone || false;
if (isDefined(timewindow.selectedTab)) {
cloned.selectedTab = timewindow.selectedTab;
if (timewindow.selectedTab === TimewindowType.REALTIME) {
cloned.realtime = deepClone(timewindow.realtime);
} else if (timewindow.selectedTab === TimewindowType.HISTORY) {
cloned.history = deepClone(timewindow.history);
}
}
cloned.realtime = deepClone(timewindow.realtime);
cloned.history = deepClone(timewindow.history);
cloned.aggregation = deepClone(timewindow.aggregation);
cloned.timezone = timewindow.timezone;
return cloned;

12
ui-ngx/src/app/shared/shared.module.ts

@ -220,11 +220,15 @@ import { ImageGalleryDialogComponent } from '@shared/components/image/image-gall
import { RuleChainSelectPanelComponent } from '@shared/components/rule-chain/rule-chain-select-panel.component';
import { WidgetButtonComponent } from '@shared/components/button/widget-button.component';
import { HexInputComponent } from '@shared/components/color-picker/hex-input.component';
import { TimezoneComponent } from '@shared/components/time/timezone.component';
import { TimezonePanelComponent } from '@shared/components/time/timezone-panel.component';
import { TimewindowConfigDialogComponent } from '@shared/components/time/timewindow-config-dialog.component';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { ScadaSymbolInputComponent } from '@shared/components/image/scada-symbol-input.component';
import { CountryAutocompleteComponent } from '@shared/components/country-autocomplete.component';
import { CountryData } from '@shared/models/country.models';
import { SvgXmlComponent } from '@shared/components/svg-xml.component';
import { DatapointsLimitComponent } from '@shared/components/time/datapoints-limit.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -309,8 +313,12 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
UserMenuComponent,
TimewindowComponent,
TimewindowPanelComponent,
TimewindowConfigDialogComponent,
TimeintervalComponent,
TimezoneComponent,
TimezonePanelComponent,
QuickTimeIntervalComponent,
DatapointsLimitComponent,
DashboardSelectComponent,
DashboardSelectPanelComponent,
DatetimePeriodComponent,
@ -518,8 +526,12 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
UserMenuComponent,
TimewindowComponent,
TimewindowPanelComponent,
TimewindowConfigDialogComponent,
TimeintervalComponent,
TimezoneComponent,
TimezonePanelComponent,
QuickTimeIntervalComponent,
DatapointsLimitComponent,
DashboardSelectComponent,
DatetimePeriodComponent,
DatetimeComponent,

12
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1114,7 +1114,9 @@
"date-from": "Date from",
"time-from": "Time from",
"date-to": "Date to",
"time-to": "Time to"
"time-to": "Time to",
"from": "From",
"to": "To"
},
"dashboard": {
"dashboard": "Dashboard",
@ -5090,6 +5092,7 @@
"minutes": "Minutes",
"seconds": "Seconds",
"advanced": "Advanced",
"custom": "Custom",
"predefined": {
"yesterday": "Yesterday",
"day-before-yesterday": "Day before yesterday",
@ -5132,6 +5135,7 @@
},
"timewindow": {
"timewindow": "Timewindow",
"timewindow-settings": "Timewindow settings",
"years": "{ years, plural, =1 { year } other {# years } }",
"years-short": "{{ years }}y",
"months": "{ months, plural, =1 { month } other {# months } }",
@ -5177,7 +5181,11 @@
"font": "Font",
"color": "Color",
"displayTypePrefix": "Display Realtime/History prefix",
"preview": "Preview"
"preview": "Preview",
"relative": "Relative",
"range": "Range",
"hide-timewindow-section": "Hide timewindow section from end-users",
"disable-custom-interval": "Disable custom interval selection"
},
"tooltip": {
"trigger": "Trigger",

44
ui-ngx/src/form.scss

@ -235,6 +235,10 @@
.fixed-title-width {
min-width: 200px;
&-180 {
min-width: 180px;
}
&-230 {
min-width: 230px;
}
@ -260,12 +264,21 @@
}
.tb-flex {
display: flex;
flex: 1;
gap: 8px;
&.no-flex {
flex: none;
}
&-xs {
@media #{$mat-xs} {
flex: 1;
}
}
}
[class*="tb-flex"] {
display: flex;
gap: 8px;
&.row {
flex-direction: row;
}
@ -287,12 +300,15 @@
&.space-between {
justify-content: space-between;
}
&.center {
justify-content: center;
}
&.align-center {
align-items: center;
}
&.align-start {
align-items: start;
}
&.align-end {
align-items: end;
}
&.no-gap {
gap: 0;
}
@ -362,10 +378,19 @@
.mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(.mdc-text-field--invalid):not(:hover) {
.mdc-notched-outline__leading, .mdc-notched-outline__trailing {
.mdc-notched-outline__leading, .mdc-notched-outline__trailing, .mdc-notched-outline__notch {
border-color: rgba(0, 0, 0, 0.12);
}
}
.mdc-floating-label {
top: 20px;
font-weight: 400;
font-size: 14px;
line-height: 20px;
&--float-above {
--mat-mdc-form-field-label-transform: translateY(-27px) scale(var(--mat-mdc-form-field-floating-label-scale, 0.75));
}
}
.mat-mdc-form-field-infix {
padding-top: 8px;
padding-bottom: 8px;
@ -377,7 +402,8 @@
line-height: 20px;
}
}
.mat-mdc-form-field-icon-prefix, .mat-mdc-form-field-icon-suffix {
.mat-mdc-form-field-icon-prefix, .mat-mdc-form-field-icon-suffix,
.mat-datetimepicker-toggle {
height: 40px;
font-size: 14px;
line-height: 40px;
@ -391,6 +417,10 @@
width: 20px;
height: 20px;
font-size: 20px;
svg {
width: 20px;
height: 20px;
}
}
.mat-mdc-button-touch-target {
width: 40px;

Loading…
Cancel
Save