Browse Source

Merge pull request #12151 from ChantsovaEkaterina/feature/advanced-timewindow-configuration

Timewindow: added ability to select (hide particular) intervals and aggregation options in panel
pull/12175/head
Igor Kulikov 2 years ago
committed by GitHub
parent
commit
90ef67bf59
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 14
      ui-ngx/src/app/modules/common/modules-map.ts
  2. 47
      ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.html
  3. 44
      ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.scss
  4. 68
      ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.ts
  5. 30
      ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.html
  6. 37
      ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.scss
  7. 156
      ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.ts
  8. 41
      ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.html
  9. 28
      ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.scss
  10. 167
      ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.ts
  11. 85
      ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.html
  12. 71
      ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.scss
  13. 231
      ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.ts
  14. 7
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.html
  15. 13
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss
  16. 80
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts
  17. 3
      ui-ngx/src/app/shared/components/time/timeinterval.component.html
  18. 8
      ui-ngx/src/app/shared/components/time/timeinterval.component.scss
  19. 76
      ui-ngx/src/app/shared/components/time/timeinterval.component.ts
  20. 60
      ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html
  21. 369
      ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts
  22. 7
      ui-ngx/src/app/shared/components/time/timewindow-form.scss
  23. 32
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  24. 6
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss
  25. 195
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
  26. 185
      ui-ngx/src/app/shared/models/time/time.models.ts
  27. 12
      ui-ngx/src/app/shared/shared.module.ts
  28. 14
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -331,11 +331,14 @@ import * as AssetProfileComponent from '@home/components/profile/asset-profile.c
import * as AssetProfileDialogComponent from '@home/components/profile/asset-profile-dialog.component';
import * as AssetProfileAutocompleteComponent from '@home/components/profile/asset-profile-autocomplete.component';
import * as RuleChainSelectComponent from '@shared/components/rule-chain/rule-chain-select.component';
import * as TimezoneComponent from '@shared/components/time/timezone.component';
import * as TimezonePanelComponent from '@shared/components/time/timezone-panel.component';
import * as DatapointsLimitComponent from '@shared/components/time/datapoints-limit.component';
import * as AggregationTypeSelectComponent from '@shared/components/time/aggregation/aggregation-type-select.component';
import * as AggregationOptionsConfigComponent from '@shared/components/time/aggregation/aggregation-options-config-panel.component';
import * as IntervalOptionsConfigPanelComponent from '@shared/components/time/interval-options-config-panel.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';
import { Observable, map, of } from 'rxjs';
import { getFlexLayout } from '@shared/legacy/flex-layout.models';
import { isJSResourceUrl } from '@shared/public-api';
@ -470,7 +473,10 @@ class ModulesMap implements IModulesMap {
'@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/time/datapoints-limit.component': DatapointsLimitComponent,
'@shared/components/time/aggregation/aggregation-type-select.component': AggregationTypeSelectComponent,
'@shared/components/time/aggregation/aggregation-options-config-panel.component': AggregationOptionsConfigComponent,
'@shared/components/time/interval-options-config-panel.component': IntervalOptionsConfigPanelComponent,
'@shared/components/value-input.component': ValueInputComponent,
'@shared/components/dashboard-autocomplete.component': DashboardAutocompleteComponent,
'@shared/components/entity/entity-subtype-autocomplete.component': EntitySubTypeAutocompleteComponent,

47
ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.html

@ -0,0 +1,47 @@
<!--
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]="aggregationOptionsConfigForm" class="tb-aggregation-options-form tb-form-panel no-border">
<div class="tb-form-panel-title">{{ 'timewindow.edit-aggregation-functions-list' | translate }}</div>
<div class="tb-form-hint tb-primary-fill">{{ 'timewindow.edit-aggregation-functions-list-hint' | translate }}</div>
<div class="tb-form-table no-gap no-padding">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell">{{"timewindow.allowed-aggregation-functions" | translate }}</div>
</div>
<div class="tb-form-table-body">
<mat-selection-list formControlName="allowedAggregationTypes">
<mat-list-option *ngFor="let type of allAggregationTypes" [value]="type" togglePosition="before" color="primary">
{{ aggregationTypesTranslations.get(aggregationTypes[type]) | translate }}
</mat-list-option>
</mat-selection-list>
</div>
</div>
<div class="tb-flex flex-end no-gap">
<button type="button"
mat-button
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="button"
mat-raised-button
color="primary"
(click)="update()"
[disabled]="aggregationOptionsConfigForm.invalid || !aggregationOptionsConfigForm.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</form>

44
ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.scss

@ -0,0 +1,44 @@
/**
* 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-aggregation-options-form {
height: 100%;
max-width: 350px;
.tb-form-table {
overflow: hidden;
}
.tb-form-table-body {
overflow-y: auto;
}
.tb-form-hint {
flex-shrink: 0;
}
.mdc-list {
display: flex;
flex-direction: column;
gap: 8px;
.mat-mdc-list-item.mdc-list-item--with-leading-checkbox.mdc-list-item--with-one-line {
height: 40px;
}
}
}
}

68
ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.ts

@ -0,0 +1,68 @@
///
/// 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 { aggregationTranslations, AggregationType } from '@shared/models/time/time.models';
import { FormBuilder, FormGroup } from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component';
@Component({
selector: 'tb-aggregation-options-config-panel',
templateUrl: './aggregation-options-config-panel.component.html',
styleUrls: ['./aggregation-options-config-panel.component.scss']
})
export class AggregationOptionsConfigPanelComponent implements OnInit {
@Input()
allowedAggregationTypes: Array<AggregationType>;
@Input()
onClose: (result: Array<AggregationType> | null) => void;
@Input()
popoverComponent: TbPopoverComponent;
aggregationOptionsConfigForm: FormGroup;
aggregationTypes = AggregationType;
allAggregationTypes: Array<AggregationType> = Object.values(AggregationType);
aggregationTypesTranslations = aggregationTranslations;
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.aggregationOptionsConfigForm = this.fb.group({
allowedAggregationTypes: [this.allowedAggregationTypes?.length ? this.allowedAggregationTypes : this.allAggregationTypes]
});
}
update() {
if (this.onClose) {
const allowedAggregationTypes = this.aggregationOptionsConfigForm.get('allowedAggregationTypes').value;
// if full list selected returns empty for optimization
this.onClose(allowedAggregationTypes?.length < this.allAggregationTypes.length ? allowedAggregationTypes : []);
}
}
cancel() {
if (this.onClose) {
this.onClose(null);
}
}
}

30
ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.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.
-->
<mat-form-field [formGroup]="aggregationTypeFormGroup"
[subscriptSizing]="subscriptSizing" [appearance]="appearance"
class="flex flex-1">
<mat-label *ngIf="displayLabel">{{ label }}</mat-label>
<mat-select [required]="required" formControlName="aggregationType">
<mat-option *ngFor="let type of aggregationTypes" [value]="type">
{{ displayAggregationTypeFn(type) }}
</mat-option>
</mat-select>
<ng-container matSuffix>
<ng-content select="[matSuffix]"></ng-content>
</ng-container>
</mat-form-field>

37
ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.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 {
.mat-mdc-form-field {
width: 100%;
}
}
:host ::ng-deep {
.mat-mdc-form-field-infix {
width: 100%;
}
.mat-mdc-select-value {
min-width: 100px;
}
.mat-mdc-form-field-has-icon-suffix {
.mat-mdc-text-field-wrapper:has(.mat-mdc-form-field-icon-suffix:empty) {
padding-right: 12px;
}
.mat-mdc-form-field-icon-suffix:empty {
padding: 0;
}
}
}

156
ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.ts

@ -0,0 +1,156 @@
///
/// 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, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { aggregationTranslations, AggregationType } from '@shared/models/time/time.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { isEqual } from '@core/utils';
@Component({
selector: 'tb-aggregation-type-select',
templateUrl: './aggregation-type-select.component.html',
styleUrls: ['./aggregation-type-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AggregationTypeSelectComponent),
multi: true
}]
})
export class AggregationTypeSelectComponent implements ControlValueAccessor, OnInit, OnChanges {
aggregationTypeFormGroup: FormGroup;
modelValue: AggregationType | null;
@Input()
allowedAggregationTypes: Array<AggregationType>;
@Input()
@coerceBoolean()
required: boolean;
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
displayLabel = true;
@Input()
labelText: string;
get label(): string {
if (this.labelText && this.labelText.length) {
return this.labelText;
}
return this.defaultLabel;
}
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
aggregationTypes: Array<AggregationType>;
private defaultLabel = this.translate.instant('aggregation.aggregation');
private allAggregationTypes: Array<AggregationType> = Object.values(AggregationType);
private propagateChange = (v: any) => { };
constructor(private translate: TranslateService,
private fb: FormBuilder) {
this.aggregationTypeFormGroup = this.fb.group({
aggregationType: [null]
});
this.aggregationTypeFormGroup.get('aggregationType').valueChanges.pipe(
takeUntilDestroyed()
).subscribe(
(value) => {
let modelValue;
if (!value) {
modelValue = null;
} else {
modelValue = value;
}
this.updateView(modelValue);
}
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.aggregationTypes = this.allowedAggregationTypes?.length ? this.allowedAggregationTypes : this.allAggregationTypes;
}
ngOnChanges({allowedAggregationTypes}: SimpleChanges): void {
if (allowedAggregationTypes && !allowedAggregationTypes.firstChange && !isEqual(allowedAggregationTypes.currentValue, allowedAggregationTypes.previousValue)) {
this.aggregationTypes = this.allowedAggregationTypes?.length ? this.allowedAggregationTypes : this.allAggregationTypes;
const currentAggregationType: AggregationType = this.aggregationTypeFormGroup.get('aggregationType').value;
if (currentAggregationType && !this.aggregationTypes.includes(currentAggregationType)) {
this.aggregationTypeFormGroup.get('aggregationType').patchValue(this.aggregationTypes[0], {emitEvent: true});
}
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.aggregationTypeFormGroup.disable();
} else {
this.aggregationTypeFormGroup.enable();
}
}
writeValue(value: AggregationType | null): void {
let aggregationType: AggregationType;
if (value && this.allowedAggregationTypes?.length && !this.allowedAggregationTypes.includes(value)) {
aggregationType = this.allowedAggregationTypes[0];
} else {
aggregationType = value;
}
this.modelValue = aggregationType;
this.aggregationTypeFormGroup.get('aggregationType').patchValue(aggregationType, {emitEvent: false});
}
updateView(value: AggregationType | null) {
if (this.modelValue !== value) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
displayAggregationTypeFn(aggregationType?: AggregationType | null): string | undefined {
if (aggregationType) {
return this.translate.instant(aggregationTranslations.get(aggregationType));
} else {
return '';
}
}
}

41
ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.html

@ -0,0 +1,41 @@
<!--
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.
-->
<section class="flex flex-1 flex-row gap-2" [formGroup]="timeintervalFormGroup">
<mat-form-field class="agg-interval flex flex-1" [class]="{'tb-inline-field': appearance === 'outline'}"
[subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-select formControlName="aggIntervals" multiple
placeholder="{{ 'timewindow.all' | translate }}">
<mat-select-trigger *ngIf="timeintervalFormGroup.get('aggIntervals').value?.length === allIntervals.length">
{{ 'timewindow.all' | translate }}
</mat-select-trigger>
<mat-option *ngFor="let interval of allIntervals" [value]="interval.value">
{{ interval.name | translate:interval.translateParams }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex flex-1" [class]="{'tb-inline-field': appearance === 'outline'}"
[subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-select formControlName="defaultAggInterval"
placeholder="{{ 'action.set' | translate }}">
<mat-option>--</mat-option>
<mat-option *ngFor="let interval of selectedIntervals" [value]="interval.value">
{{ interval.name | translate:interval.translateParams }}
</mat-option>
</mat-select>
</mat-form-field>
</section>

28
ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.scss

@ -0,0 +1,28 @@
/**
* 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';
:host {
.agg-interval {
max-width: 180px;
}
@media #{$mat-xs} {
min-width: 0;
width: 100%;
}
}

167
ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.ts

@ -0,0 +1,167 @@
///
/// 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, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TimeService } from '@core/services/time.service';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion';
import {
Interval,
intervalValuesToTimeIntervals,
TimeInterval,
TimewindowAggIntervalOptions
} from '@shared/models/time/time.models';
import { isDefined } from '@core/utils';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'tb-grouping-interval-options',
templateUrl: './grouping-interval-options.component.html',
styleUrls: ['./grouping-interval-options.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GroupingIntervalOptionsComponent),
multi: true
}
]
})
export class GroupingIntervalOptionsComponent implements OnInit, ControlValueAccessor {
@Input()
@coerceNumber()
min: number;
@Input()
@coerceNumber()
max: number;
@Input()
@coerceBoolean()
useCalendarIntervals = false;
@Input() disabled: boolean;
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
allIntervals: Array<TimeInterval>;
allIntervalValues: Array<Interval>;
selectedIntervals: Array<TimeInterval>;
timeintervalFormGroup: FormGroup;
private modelValue: TimewindowAggIntervalOptions;
private rendered = false;
private propagateChangeValue: any;
private propagateChange = (value: any) => {
this.propagateChangeValue = value;
};
constructor(private timeService: TimeService,
private fb: FormBuilder) {
this.timeintervalFormGroup = this.fb.group({
aggIntervals: [ [] ],
defaultAggInterval: [ null ],
});
this.timeintervalFormGroup.get('aggIntervals').valueChanges.pipe(
takeUntilDestroyed()
).subscribe(selectedIntervalValues => this.setSelectedIntervals(selectedIntervalValues));
this.timeintervalFormGroup.valueChanges.pipe(
takeUntilDestroyed()
).subscribe(() => this.updateView());
}
ngOnInit(): void {
this.updateIntervalsList();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
if (isDefined(this.propagateChangeValue)) {
this.propagateChange(this.propagateChangeValue);
}
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.timeintervalFormGroup.disable({emitEvent: false});
} else {
this.timeintervalFormGroup.enable({emitEvent: false});
}
}
writeValue(intervalOptions: TimewindowAggIntervalOptions): void {
this.modelValue = intervalOptions;
this.rendered = true;
this.timeintervalFormGroup.get('defaultAggInterval').patchValue(intervalOptions.defaultAggInterval, { emitEvent: false });
this.setIntervals(intervalOptions.aggIntervals);
}
private updateIntervalsList() {
this.allIntervals = this.timeService.getIntervals(this.min, this.max, this.useCalendarIntervals);
this.allIntervalValues = this.allIntervals.map(interval => interval.value);
}
private setIntervals(intervals: Array<Interval>) {
const selectedIntervals = !intervals?.length ? this.allIntervalValues : intervals;
this.timeintervalFormGroup.get('aggIntervals').patchValue(
selectedIntervals,
{emitEvent: false});
this.setSelectedIntervals(selectedIntervals);
}
private setSelectedIntervals(selectedIntervalValues: Array<Interval>) {
if (!selectedIntervalValues.length || selectedIntervalValues.length === this.allIntervalValues.length) {
this.selectedIntervals = this.allIntervals;
} else {
this.selectedIntervals = intervalValuesToTimeIntervals(selectedIntervalValues);
}
const defaultInterval: Interval = this.timeintervalFormGroup.get('defaultAggInterval').value;
if (defaultInterval && !selectedIntervalValues.includes(defaultInterval)) {
this.timeintervalFormGroup.get('defaultAggInterval').patchValue(null);
}
}
private updateView() {
if (!this.rendered) {
return;
}
let selectedIntervals: Array<Interval>;
const intervals: Array<Interval> = this.timeintervalFormGroup.get('aggIntervals').value;
if (intervals.length < this.allIntervals.length) {
selectedIntervals = intervals;
} else {
selectedIntervals = [];
}
this.modelValue = {
aggIntervals: selectedIntervals,
defaultAggInterval: this.timeintervalFormGroup.get('defaultAggInterval').value
};
this.propagateChange(this.modelValue);
}
}

85
ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.html

@ -0,0 +1,85 @@
<!--
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]="intervalOptionsConfigForm" class="tb-interval-options-form tb-form-panel no-border"
[style]="aggregation ? {maxWidth: '600px'} : {maxWidth: '350px'}">
<div class="tb-form-panel-title">{{ 'timewindow.edit-intervals-list' | translate }}</div>
<div class="tb-form-hint tb-primary-fill">
{{ 'timewindow.edit-intervals-list-hint' | translate }} {{ aggregation ? ('timewindow.edit-grouping-intervals-list-hint' | translate) : '' }}
</div>
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell tb-interval" tbTruncateWithTooltip>
<mat-checkbox color="warn"
(click)="$event.stopPropagation()"
[indeterminate]="getIndeterminate()"
(change)="enableDisableIntervals($event.checked)"
[checked]="getChecked()">
</mat-checkbox>
{{ "timewindow.interval" | translate }}
</div>
<ng-container *ngIf="aggregation">
<div class="tb-form-table-header-cell tb-agg-interval-header" tbTruncateWithTooltip>{{"timewindow.allowed-agg-intervals" | translate }}</div>
<div class="tb-form-table-header-cell tb-agg-interval-header" tbTruncateWithTooltip>{{"timewindow.default-agg-interval" | translate }}</div>
</ng-container>
</div>
<div class="tb-form-table-body" formArrayName="intervals">
<ng-container *ngFor="let interval of intervalsFormArray.controls; let i = index; trackBy: trackByElement">
<div [formGroupName]="i" class="tb-form-table-row">
<div class="tb-form-table-row-cell tb-interval">
<mat-checkbox class="tb-interval-option" formControlName="enabled" color="primary">
<mat-label tbTruncateWithTooltip>{{ interval.get('name').value }}</mat-label>
</mat-checkbox>
</div>
<ng-container *ngIf="aggregation">
<div class="tb-form-table-row-cell tb-agg-interval">
<tb-grouping-interval-options
formControlName="aggIntervalsConfig"
[min]="minAggInterval(interval.get('value').value)"
[max]="maxAggInterval(interval.get('value').value)"
useCalendarIntervals
subscriptSizing="dynamic"
appearance="outline">
</tb-grouping-interval-options>
</div>
</ng-container>
</div>
</ng-container>
</div>
</div>
<div class="flex flex-row gap-2 items-center">
<button type="button"
mat-button
color="primary"
(click)="reset()">
{{ 'action.reset' | translate }}
</button>
<span class="flex-1"></span>
<button type="button"
mat-button
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="button"
mat-raised-button
color="primary"
(click)="update()"
[disabled]="intervalOptionsConfigForm.invalid || !intervalOptionsConfigForm.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</form>

71
ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.scss

@ -0,0 +1,71 @@
/**
* 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';
:host {
.tb-interval-options-form {
height: 100%;
.tb-form-table {
overflow: hidden;
}
.tb-form-table-body {
overflow-y: auto;
}
.tb-form-table-header-cell, .tb-form-table-row-cell {
&.tb-interval {
flex: 1 1 30%;
min-width: calc(30% - 8px);
@media #{$mat-gt-md} {
min-width: calc(30% - 12px);
}
}
&.tb-agg-interval {
flex: 1 1 70%;
min-width: 70%;
}
}
.tb-form-table-header-cell.tb-agg-interval-header {
flex: 1 1 35%;
width: 35%;
}
.tb-form-hint {
flex-shrink: 0;
}
::ng-deep {
.tb-interval-option {
display: block;
.mdc-form-field {
width: 100%;
}
.mdc-label {
overflow: hidden;
mat-label {
display: block;
}
}
}
}
}
}

231
ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.ts

@ -0,0 +1,231 @@
///
/// 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, DestroyRef, inject, Input, OnInit } from '@angular/core';
import {
HistoryWindowType,
QuickTimeInterval, quickTimeIntervalPeriod,
QuickTimeIntervalTranslationMap,
RealtimeWindowType,
TimewindowAggIntervalOptions,
TimewindowAggIntervalsConfig,
TimewindowInterval,
TimewindowIntervalOption,
TimewindowType
} from '@shared/models/time/time.models';
import { AbstractControl, FormBuilder, FormGroup, UntypedFormArray } from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { TimeService } from '@core/services/time.service';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TranslateService } from '@ngx-translate/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export interface IntervalOptionsConfigPanelData {
allowedIntervals: Array<TimewindowInterval>;
aggIntervalsConfig: TimewindowAggIntervalsConfig
}
@Component({
selector: 'tb-interval-options-config-panel',
templateUrl: './interval-options-config-panel.component.html',
styleUrls: ['./interval-options-config-panel.component.scss']
})
export class IntervalOptionsConfigPanelComponent implements OnInit {
@Input()
@coerceBoolean()
aggregation = false;
@Input()
allowedIntervals: Array<TimewindowInterval>;
@Input()
aggIntervalsConfig: TimewindowAggIntervalsConfig;
@Input()
intervalType: RealtimeWindowType | HistoryWindowType;
@Input()
timewindowType: TimewindowType;
@Input()
onClose: (result: IntervalOptionsConfigPanelData | null) => void;
@Input()
popoverComponent: TbPopoverComponent;
intervalOptionsConfigForm: FormGroup;
allIntervals: Array<TimewindowIntervalOption>;
allIntervalValues: Array<TimewindowInterval>
private timeIntervalTranslationMap = QuickTimeIntervalTranslationMap;
private destroyRef = inject(DestroyRef);
constructor(private fb: FormBuilder,
private timeService: TimeService,
private translate: TranslateService) {}
ngOnInit(): void {
if (this.intervalType === RealtimeWindowType.LAST_INTERVAL ||
this.intervalType === HistoryWindowType.LAST_INTERVAL) {
this.allIntervals = this.timeService.getIntervals(undefined, undefined, false);
this.allIntervalValues = this.allIntervals.map(interval => interval.value);
} else {
this.allIntervalValues = this.getQuickIntervals();
this.allIntervals = this.allIntervalValues.map(interval => ({
name: this.timeIntervalTranslationMap.get(interval as QuickTimeInterval),
value: interval
}));
}
this.intervalOptionsConfigForm = this.fb.group({
intervals: this.fb.array([])
});
const intervalControls: Array<AbstractControl> = [];
for (const interval of this.allIntervals) {
const intervalConfig: TimewindowAggIntervalOptions = this.aggIntervalsConfig?.hasOwnProperty(interval.value)
? this.aggIntervalsConfig[interval.value]
: null;
const intervalEnabled = this.allowedIntervals?.length ? this.allowedIntervals.includes(interval.value) : true;
const intervalControl = this.fb.group({
name: [this.translate.instant(interval.name, interval.translateParams)],
value: [interval.value],
enabled: [intervalEnabled],
aggIntervalsConfig: [{value: {
aggIntervals: intervalConfig?.aggIntervals ? intervalConfig.aggIntervals : [],
defaultAggInterval: intervalConfig?.defaultAggInterval ? intervalConfig.defaultAggInterval : null
}, disabled: !(intervalEnabled && this.aggregation)}]
});
if (this.aggregation) {
intervalControl.get('enabled').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((intervalEnabled) => {
if (intervalEnabled) {
intervalControl.get('aggIntervalsConfig').enable({emitEvent: false});
} else {
intervalControl.get('aggIntervalsConfig').disable({emitEvent: false});
}
});
}
intervalControls.push(intervalControl);
}
this.intervalOptionsConfigForm.setControl('intervals', this.fb.array(intervalControls), {emitEvent: false});
}
get intervalsFormArray(): UntypedFormArray {
return this.intervalOptionsConfigForm.get('intervals') as UntypedFormArray;
}
minAggInterval(interval: TimewindowInterval) {
return this.timeService.minIntervalLimit(this.getIntervalMs(interval));
}
maxAggInterval(interval: TimewindowInterval) {
return this.timeService.maxIntervalLimit(this.getIntervalMs(interval));
}
private getIntervalMs(interval: TimewindowInterval): number {
if (this.intervalType === RealtimeWindowType.INTERVAL ||
this.intervalType === HistoryWindowType.INTERVAL) {
return quickTimeIntervalPeriod(interval as QuickTimeInterval);
}
return interval as number;
}
trackByElement(i: number, item: any) {
return item;
}
update() {
if (this.onClose) {
const allowedIntervals: Array<TimewindowInterval> = [];
const aggIntervalsConfig: TimewindowAggIntervalsConfig = {};
const intervalOptionsConfig = this.intervalOptionsConfigForm.get('intervals').value;
for (const interval of intervalOptionsConfig) {
if (interval.enabled) {
allowedIntervals.push(interval.value);
if (this.aggregation && (interval.aggIntervalsConfig.aggIntervals.length || interval.aggIntervalsConfig.defaultAggInterval)) {
const intervalParams: TimewindowAggIntervalOptions = {};
if (interval.aggIntervalsConfig.aggIntervals.length) {
intervalParams.aggIntervals = interval.aggIntervalsConfig.aggIntervals;
}
if (interval.aggIntervalsConfig.defaultAggInterval) {
intervalParams.defaultAggInterval = interval.aggIntervalsConfig.defaultAggInterval;
}
aggIntervalsConfig[interval.value] = intervalParams;
}
}
}
this.onClose({
// if full list selected returns empty for optimization
allowedIntervals: allowedIntervals?.length < this.allIntervals.length ? allowedIntervals : [],
aggIntervalsConfig
});
}
}
cancel() {
if (this.onClose) {
this.onClose(null);
}
}
reset() {
const intervalControls = this.intervalsFormArray.controls;
for (const interval of intervalControls) {
interval.patchValue({
enabled: true,
aggIntervalsConfig: {
aggIntervals: [],
defaultAggInterval: null
}
});
}
this.intervalOptionsConfigForm.markAsDirty();
}
private getQuickIntervals(): Array<QuickTimeInterval> {
const allQuickIntervals = Object.values(QuickTimeInterval);
if (this.timewindowType === TimewindowType.REALTIME) {
return allQuickIntervals.filter(interval => interval.startsWith('CURRENT_'));
}
return allQuickIntervals;
}
getIndeterminate(): boolean {
const enabledIntervals = this.intervalsFormArray.value.filter(interval => interval.enabled);
return enabledIntervals.length !== 0 && enabledIntervals.length !== this.allIntervals.length;
}
enableDisableIntervals(allEnabled: boolean) {
const intervalControls = this.intervalsFormArray.controls;
for (const interval of intervalControls) {
interval.patchValue({
enabled: allEnabled
});
}
this.intervalOptionsConfigForm.markAsDirty();
}
getChecked(): boolean {
const intervals = this.intervalsFormArray.value;
return intervals.every(interval => interval.enabled);
}
}

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

@ -15,13 +15,16 @@
limitations under the License.
-->
<section class="interval-section flex flex-1 flex-row">
<section class="interval-section flex flex-1 flex-row" [formGroup]="quickIntervalFormGroup">
<mat-form-field class="flex-1" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label *ngIf="displayLabel" translate>timewindow.interval</mat-label>
<mat-select [disabled]="disabled" [(ngModel)]="modelValue" (ngModelChange)="onIntervalChange()">
<mat-select formControlName="interval">
<mat-option *ngFor="let interval of intervals" [value]="interval">
{{ timeIntervalTranslationMap.get(interval) | translate}}
</mat-option>
</mat-select>
<ng-container matSuffix>
<ng-content select="[matSuffix]"></ng-content>
</ng-container>
</mat-form-field>
</section>

13
ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss

@ -16,7 +16,7 @@
@import '../../../../scss/constants';
:host {
min-width: 355px;
min-width: 300px;
display: block;
@media #{$mat-xs} {
@ -24,3 +24,14 @@
width: 100%;
}
}
:host ::ng-deep {
.mat-mdc-form-field-has-icon-suffix {
.mat-mdc-text-field-wrapper:has(.mat-mdc-form-field-icon-suffix:empty) {
padding-right: 12px;
}
.mat-mdc-form-field-icon-suffix:empty {
padding: 0;
}
}
}

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

@ -14,11 +14,13 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, 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';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { isEqual } from '@core/utils';
@Component({
selector: 'tb-quick-time-interval',
@ -32,9 +34,9 @@ import { coerceBoolean } from '@shared/decorators/coercion';
}
]
})
export class QuickTimeIntervalComponent implements OnInit, ControlValueAccessor {
export class QuickTimeIntervalComponent implements OnInit, ControlValueAccessor, OnChanges {
private allIntervals = Object.values(QuickTimeInterval);
private allIntervals = Object.values(QuickTimeInterval) as QuickTimeInterval[];
modelValue: QuickTimeInterval;
timeIntervalTranslationMap = QuickTimeIntervalTranslationMap;
@ -49,25 +51,53 @@ export class QuickTimeIntervalComponent implements OnInit, ControlValueAccessor
@Input() onlyCurrentInterval = false;
@Input()
allowedIntervals: Array<QuickTimeInterval>
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
intervals: Array<QuickTimeInterval>;
private allAvailableIntervals: Array<QuickTimeInterval>;
quickIntervalFormGroup: FormGroup;
private propagateChange = (_: any) => {};
constructor() {
constructor(private fb: FormBuilder) {
this.quickIntervalFormGroup = this.fb.group({
interval: [ null ]
});
this.quickIntervalFormGroup.get('interval').valueChanges.pipe(
takeUntilDestroyed()
).subscribe((value) => {
let modelValue;
if (!value) {
modelValue = null;
} else {
modelValue = value;
}
this.updateView(modelValue);
});
}
get intervals() {
if (this.onlyCurrentInterval) {
return this.allIntervals.filter(interval => interval.startsWith('CURRENT_'));
}
return this.allIntervals;
ngOnInit(): void {
this.allAvailableIntervals = this.getAllAvailableIntervals();
this.intervals = this.allowedIntervals?.length ? this.allowedIntervals : this.allAvailableIntervals;
}
ngOnInit(): void {
ngOnChanges({allowedIntervals}: SimpleChanges): void {
if (allowedIntervals && !allowedIntervals.firstChange && !isEqual(allowedIntervals.currentValue, allowedIntervals.previousValue)) {
this.intervals = this.allowedIntervals?.length ? this.allowedIntervals : this.allAvailableIntervals;
const currentInterval: QuickTimeInterval = this.quickIntervalFormGroup.get('interval').value;
if (currentInterval && !this.intervals.includes(currentInterval)) {
this.quickIntervalFormGroup.get('interval').patchValue(this.intervals[0], {emitEvent: true});
}
}
}
registerOnChange(fn: any): void {
@ -79,13 +109,35 @@ export class QuickTimeIntervalComponent implements OnInit, ControlValueAccessor
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.quickIntervalFormGroup.disable({emitEvent: false});
} else {
this.quickIntervalFormGroup.enable({emitEvent: false});
}
}
writeValue(interval: QuickTimeInterval): void {
writeValue(value: QuickTimeInterval): void {
let interval: QuickTimeInterval;
if (value && this.allowedIntervals?.length && !this.allowedIntervals.includes(value)) {
interval = this.allowedIntervals[0];
} else {
interval = value;
}
this.modelValue = interval;
this.quickIntervalFormGroup.get('interval').patchValue(interval, {emitEvent: false});
}
updateView(value: QuickTimeInterval | null) {
if (this.modelValue !== value) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
onIntervalChange() {
this.propagateChange(this.modelValue);
private getAllAvailableIntervals() {
if (this.onlyCurrentInterval) {
return this.allIntervals.filter(interval => interval.startsWith('CURRENT_'));
}
return this.allIntervals;
}
}

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

@ -25,6 +25,9 @@
{{ interval.name | translate:interval.translateParams }}
</mat-option>
</mat-select>
<ng-container matSuffix>
<ng-content select="[matSuffix]"></ng-content>
</ng-container>
</mat-form-field>
</section>
<section class="tb-form-row column-xs no-border no-padding tb-standard-fields advanced-input"

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

@ -27,6 +27,14 @@
}
:host ::ng-deep {
.mat-mdc-form-field-has-icon-suffix {
.mat-mdc-text-field-wrapper:has(.mat-mdc-form-field-icon-suffix:empty) {
padding-right: 12px;
}
.mat-mdc-form-field-icon-suffix:empty {
padding: 0;
}
}
.number-input {
.mat-mdc-form-field-infix {
width: 50px;

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

@ -14,14 +14,23 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
booleanAttribute,
Component,
forwardRef,
Input,
OnChanges,
OnDestroy,
OnInit,
SimpleChanges
} 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 { 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 { Interval, IntervalMath, intervalValuesToTimeIntervals, TimeInterval } from '@shared/models/time/time.models';
import { isDefined, isEqual } from '@core/utils';
import { IntervalType } from '@shared/models/telemetry/telemetry.models';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@ -38,13 +47,11 @@ import { Subject } from 'rxjs';
}
]
})
export class TimeintervalComponent implements OnInit, ControlValueAccessor, OnDestroy {
export class TimeintervalComponent implements OnInit, ControlValueAccessor, OnChanges, OnDestroy {
minValue: number;
maxValue: number;
disabledAdvancedState = false;
@Input()
set min(min: number) {
const minValueData = coerceNumberProperty(min);
@ -67,18 +74,11 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor, OnDe
@Input() predefinedName: string;
@Input()
@coerceBoolean()
isEdit = false;
@Input({transform : booleanAttribute})
disabledAdvanced = false;
@Input()
@coerceBoolean()
set disabledAdvanced(disabledAdvanced: boolean) {
if (this.disabledAdvancedState !== disabledAdvanced) {
this.disabledAdvancedState = disabledAdvanced;
this.updateIntervalValue(true);
}
}
allowedIntervals: Array<Interval>;
@Input()
@coerceBoolean()
@ -155,6 +155,13 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor, OnDe
this.boundInterval();
}
ngOnChanges({disabledAdvanced, allowedIntervals}: SimpleChanges): void {
if ((disabledAdvanced && !disabledAdvanced.firstChange && disabledAdvanced.currentValue !== disabledAdvanced.previousValue) ||
(allowedIntervals && !allowedIntervals.firstChange && !isEqual(allowedIntervals.currentValue, allowedIntervals.previousValue))) {
this.updateIntervalValue(true);
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
if (isDefined(this.propagateChangeValue)) {
@ -184,10 +191,13 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor, OnDe
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) {
const advanced = !this.timeService.matchesExistingInterval(this.minValue, this.maxValue, this.modelValue,
if (this.allowedIntervals?.length ||
IntervalMath.numberValue(this.modelValue) >= min && IntervalMath.numberValue(this.modelValue) <= max) {
const advanced = this.allowedIntervals?.length
? !this.allowedIntervals.includes(this.modelValue)
: !this.timeService.matchesExistingInterval(this.minValue, this.maxValue, this.modelValue,
this.useCalendarIntervals);
if (advanced && this.disabledAdvancedState) {
if (advanced && this.disabledAdvanced) {
this.advanced = false;
this.boundInterval();
} else {
@ -225,20 +235,28 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor, OnDe
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 = this.allowedIntervals?.length
? intervalValuesToTimeIntervals(this.allowedIntervals)
: this.timeService.getIntervals(this.minValue, this.maxValue, this.useCalendarIntervals);
if (!this.disabledAdvanced) {
this.intervals.push(this.customTimeInterval);
}
if (this.rendered) {
let newInterval = this.modelValue;
const newIntervalMs = IntervalMath.numberValue(newInterval);
if (newIntervalMs < min) {
newInterval = min;
} else if (newIntervalMs >= max && updateToPreferred) {
newInterval = this.timeService.boundMaxInterval(max / 7);
}
if (!this.advanced) {
newInterval = this.timeService.boundToPredefinedInterval(min, max, newInterval, this.useCalendarIntervals);
if (this.allowedIntervals?.length) {
if (!this.allowedIntervals.includes(newInterval) && !this.advanced) {
newInterval = this.allowedIntervals[0];
}
} else {
const newIntervalMs = IntervalMath.numberValue(newInterval);
if (newIntervalMs < min) {
newInterval = min;
} else if (newIntervalMs >= max && updateToPreferred) {
newInterval = this.timeService.boundMaxInterval(max / 7);
}
if (!this.advanced) {
newInterval = this.timeService.boundToPredefinedInterval(min, max, newInterval, this.useCalendarIntervals);
}
}
if (newInterval !== this.modelValue) {
this.setInterval(newInterval);

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

@ -51,7 +51,7 @@
{{ 'timewindow.disable-custom-interval' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<div class="tb-form-row column-xs">
<mat-slide-toggle *ngIf="!quickIntervalOnly"
class="mat-slide" formControlName="hideLastInterval">
<div tb-hint-tooltip-icon="{{'timewindow.hide-last-interval' | translate}}">
@ -62,15 +62,22 @@
formControlName="timewindowMs"
subscriptSizing="dynamic"
appearance="outline"
[allowedIntervals]="timewindowForm.get('realtime.advancedParams.allowedLastIntervals').value"
[disabledAdvanced]="timewindowForm.get('realtime.disableCustomInterval').value"
[required]="timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL">
<button *ngIf="!(timewindowForm.get('realtime.hideLastInterval').value ||
timewindowForm.get('realtime.hideInterval').value)"
matSuffix mat-icon-button type="button" class="tb-mat-24"
(click)="configureRealtimeLastIntervalOptions($event)">
<mat-icon>edit</mat-icon>
</button>
</tb-timeinterval>
</div>
</ng-container>
<div *ngIf="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
class="tb-form-row">
class="tb-form-row column-xs">
<mat-slide-toggle *ngIf="!quickIntervalOnly"
class="mat-slide" formControlName="hideQuickInterval">
<div tb-hint-tooltip-icon="{{'timewindow.hide-relative-interval' | translate}}">
@ -83,8 +90,15 @@
onlyCurrentInterval="true"
subscriptSizing="dynamic"
appearance="outline"
[allowedIntervals]="timewindowForm.get('realtime.advancedParams.allowedQuickIntervals').value"
[required]="timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL">
<button *ngIf="!(timewindowForm.get('realtime.hideQuickInterval').value ||
timewindowForm.get('realtime.hideInterval').value)"
matSuffix mat-icon-button type="button" class="tb-mat-24"
(click)="configureRealtimeQuickIntervalOptions($event)">
<mat-icon>edit</mat-icon>
</button>
</tb-quick-time-interval>
</div>
</section>
@ -107,7 +121,7 @@
{{ 'timewindow.disable-custom-interval' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide" formControlName="hideLastInterval">
<div tb-hint-tooltip-icon="{{'timewindow.hide-last-interval' | translate}}">
{{ 'timewindow.hide' | translate }}
@ -117,14 +131,21 @@
formControlName="timewindowMs"
subscriptSizing="dynamic"
appearance="outline"
[allowedIntervals]="timewindowForm.get('history.advancedParams.allowedLastIntervals').value"
[disabledAdvanced]="timewindowForm.get('history.disableCustomInterval').value"
[required]="timewindowForm.get('selectedTab').value === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL">
<button *ngIf="!(timewindowForm.get('history.hideLastInterval').value ||
timewindowForm.get('history.hideInterval').value)"
matSuffix mat-icon-button type="button" class="tb-mat-24"
(click)="configureHistoryLastIntervalOptions($event)">
<mat-icon>edit</mat-icon>
</button>
</tb-timeinterval>
</div>
</ng-container>
<div *ngIf="timewindowForm.get('history.historyType').value === historyTypes.FIXED" class="tb-form-row">
<div *ngIf="timewindowForm.get('history.historyType').value === historyTypes.FIXED" class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide" formControlName="hideFixedInterval">
<div tb-hint-tooltip-icon="{{'timewindow.hide-fixed-interval' | translate}}">
{{ 'timewindow.hide' | translate }}
@ -140,7 +161,7 @@
</tb-datetime-period>
</div>
<div *ngIf="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL" class="tb-form-row">
<div *ngIf="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL" class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide" formControlName="hideQuickInterval">
<div tb-hint-tooltip-icon="{{'timewindow.hide-relative-interval' | translate}}">
{{ 'timewindow.hide' | translate }}
@ -151,8 +172,15 @@
formControlName="quickInterval"
subscriptSizing="dynamic"
appearance="outline"
[allowedIntervals]="timewindowForm.get('history.advancedParams.allowedQuickIntervals').value"
[required]="timewindowForm.get('selectedTab').value === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.INTERVAL">
<button *ngIf="!(timewindowForm.get('history.hideQuickInterval').value ||
timewindowForm.get('history.hideInterval').value)"
matSuffix mat-icon-button type="button" class="tb-mat-24"
(click)="configureHistoryQuickIntervalOptions($event)">
<mat-icon>edit</mat-icon>
</button>
</tb-quick-time-interval>
</div>
</section>
@ -167,13 +195,15 @@
</div>
</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>
<tb-aggregation-type-select class="flex" subscriptSizing="dynamic" appearance="outline" displayLabel="false"
formControlName="type"
[allowedAggregationTypes]="timewindowForm.get('allowedAggTypes').value">
<button *ngIf="!timewindowForm.get('hideAggregation').value"
matSuffix mat-icon-button type="button" class="tb-mat-24"
(click)="openAggregationOptionsConfig($event)">
<mat-icon>edit</mat-icon>
</button>
</tb-aggregation-type-select>
</ng-container>
</div>
</section>
@ -210,7 +240,8 @@
</ng-container>
<tb-timeinterval
formControlName="interval"
[min]="minRealtimeAggInterval()" [max]="maxRealtimeAggInterval()"
[min]="minRealtimeAggInterval" [max]="maxRealtimeAggInterval"
[allowedIntervals]="realtimeAllowedAggIntervals"
useCalendarIntervals
subscriptSizing="dynamic"
appearance="outline"
@ -231,7 +262,8 @@
<ng-container formGroupName="history">
<tb-timeinterval
formControlName="interval"
[min]="minHistoryAggInterval()" [max]="maxHistoryAggInterval()"
[min]="minHistoryAggInterval" [max]="maxHistoryAggInterval"
[allowedIntervals]="historyAllowedAggIntervals"
useCalendarIntervals
subscriptSizing="dynamic"
appearance="outline"

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

@ -14,30 +14,42 @@
/// limitations under the License.
///
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import {
aggregationTranslations,
AggregationType,
DAY,
currentHistoryTimewindow,
currentRealtimeTimewindow,
historyAllowedAggIntervals,
HistoryWindowType,
historyWindowTypeTranslations,
quickTimeIntervalPeriod,
Interval,
realtimeAllowedAggIntervals,
RealtimeWindowType,
realtimeWindowTypeTranslations,
Timewindow,
TimewindowType
TimewindowAggIntervalsConfig,
TimewindowType,
updateFormValuesOnTimewindowTypeChange
} from '@shared/models/time/time.models';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TimeService } from '@core/services/time.service';
import { isDefined, isDefinedAndNotNull, mergeDeep } from '@core/utils';
import { deepClone, isDefined, isDefinedAndNotNull, isObject, 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';
import { TbPopoverService } from '@shared/components/popover.service';
import {
AggregationOptionsConfigPanelComponent
} from '@shared/components/time/aggregation/aggregation-options-config-panel.component';
import {
IntervalOptionsConfigPanelComponent,
IntervalOptionsConfigPanelData
} from '@shared/components/time/interval-options-config-panel.component';
export interface TimewindowConfigDialogData {
quickIntervalOnly: boolean;
@ -66,10 +78,6 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
aggregationTypes = AggregationType;
aggregations = Object.keys(AggregationType);
aggregationTypesTranslations = aggregationTranslations;
result: Timewindow;
timewindowTypeOptions: ToggleHeaderOption[] = [
@ -116,7 +124,11 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
protected store: Store<AppState>,
public fb: FormBuilder,
private timeService: TimeService,
private translate: TranslateService) {
private translate: TranslateService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private cd: ChangeDetectorRef,
public viewContainerRef: ViewContainerRef) {
super(store);
this.quickIntervalOnly = data.quickIntervalOnly;
this.aggregation = data.aggregation;
@ -159,7 +171,17 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
value: isDefinedAndNotNull(this.timewindow.realtime.hideQuickInterval)
? this.timewindow.realtime.hideQuickInterval : false,
disabled: this.timewindow.realtime.hideInterval
}]
}],
advancedParams: this.fb.group({
allowedLastIntervals: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.allowedLastIntervals)
? this.timewindow.realtime.advancedParams.allowedLastIntervals : null ],
allowedQuickIntervals: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.allowedQuickIntervals)
? this.timewindow.realtime.advancedParams.allowedQuickIntervals : null ],
lastAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.lastAggIntervalsConfig)
? this.timewindow.realtime.advancedParams.lastAggIntervalsConfig : null ],
quickAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.quickAggIntervalsConfig)
? this.timewindow.realtime.advancedParams.quickAggIntervalsConfig : null ]
})
}),
history: this.fb.group({
historyType: [ isDefined(history?.historyType) ? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL ],
@ -187,13 +209,25 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
value: isDefinedAndNotNull(this.timewindow.history.hideFixedInterval)
? this.timewindow.history.hideFixedInterval : false,
disabled: this.timewindow.history.hideInterval
}]
}],
advancedParams: this.fb.group({
allowedLastIntervals: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.allowedLastIntervals)
? this.timewindow.history.advancedParams.allowedLastIntervals : null ],
allowedQuickIntervals: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.allowedQuickIntervals)
? this.timewindow.history.advancedParams.allowedQuickIntervals : null ],
lastAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.lastAggIntervalsConfig)
? this.timewindow.history.advancedParams.lastAggIntervalsConfig : null ],
quickAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.quickAggIntervalsConfig)
? this.timewindow.history.advancedParams.quickAggIntervalsConfig : null ]
})
}),
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 ],
allowedAggTypes: [ isDefinedAndNotNull(this.timewindow.allowedAggTypes)
? this.timewindow.allowedAggTypes : null ],
hideAggregation: [ isDefinedAndNotNull(this.timewindow.hideAggregation)
? this.timewindow.hideAggregation : false ],
hideAggInterval: [ isDefinedAndNotNull(this.timewindow.hideAggInterval)
@ -201,13 +235,65 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
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);
});
if (this.aggregation) {
this.timewindowForm.get('realtime.timewindowMs').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((timewindowMs: number) => {
const lastAggIntervalsConfig: TimewindowAggIntervalsConfig =
this.timewindowForm.get('realtime.advancedParams.lastAggIntervalsConfig').value;
if (lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) &&
lastAggIntervalsConfig[timewindowMs].defaultAggInterval) {
this.timewindowForm.get('realtime.interval').patchValue(
lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false}
);
}
});
this.timewindowForm.get('realtime.quickInterval').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((quickInterval: number) => {
const quickAggIntervalsConfig: TimewindowAggIntervalsConfig =
this.timewindowForm.get('realtime.advancedParams.quickAggIntervalsConfig').value;
if (quickAggIntervalsConfig?.hasOwnProperty(quickInterval) &&
quickAggIntervalsConfig[quickInterval].defaultAggInterval) {
this.timewindowForm.get('realtime.interval').patchValue(
quickAggIntervalsConfig[quickInterval].defaultAggInterval, {emitEvent: false}
);
}
});
this.timewindowForm.get('history.timewindowMs').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((timewindowMs: number) => {
const lastAggIntervalsConfig: TimewindowAggIntervalsConfig =
this.timewindowForm.get('history.advancedParams.lastAggIntervalsConfig').value;
if (lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) &&
lastAggIntervalsConfig[timewindowMs].defaultAggInterval) {
this.timewindowForm.get('history.interval').patchValue(
lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false}
);
}
});
this.timewindowForm.get('history.quickInterval').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((quickInterval: number) => {
const quickAggIntervalsConfig: TimewindowAggIntervalsConfig =
this.timewindowForm.get('history.advancedParams.quickAggIntervalsConfig').value;
if (quickAggIntervalsConfig?.hasOwnProperty(quickInterval) &&
quickAggIntervalsConfig[quickInterval].defaultAggInterval) {
this.timewindowForm.get('history.interval').patchValue(
quickAggIntervalsConfig[quickInterval].defaultAggInterval, {emitEvent: false}
);
}
});
this.timewindowForm.get('aggregation.type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((aggregationType: AggregationType) => {
this.updateValidators(aggregationType);
});
}
this.timewindowForm.get('selectedTab').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((selectedTab: TimewindowType) => {
@ -223,6 +309,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
this.timewindowForm.get('realtime.hideLastInterval').enable({emitEvent: false});
this.timewindowForm.get('realtime.hideQuickInterval').enable({emitEvent: false});
}
this.updateDisableAdvancedOptionState('realtime.disableCustomInterval', value);
});
this.timewindowForm.get('realtime.hideLastInterval').valueChanges.pipe(
takeUntil(this.destroy$)
@ -230,6 +317,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
if (hideLastInterval && !this.timewindowForm.get('realtime.hideQuickInterval').value) {
this.timewindowForm.get('realtime.realtimeType').setValue(RealtimeWindowType.INTERVAL);
}
this.updateDisableAdvancedOptionState('realtime.disableCustomInterval', hideLastInterval);
});
this.timewindowForm.get('realtime.hideQuickInterval').valueChanges.pipe(
takeUntil(this.destroy$)
@ -251,6 +339,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
this.timewindowForm.get('history.hideQuickInterval').enable({emitEvent: false});
this.timewindowForm.get('history.hideFixedInterval').enable({emitEvent: false});
}
this.updateDisableAdvancedOptionState('history.disableCustomInterval', value);
});
this.timewindowForm.get('history.hideLastInterval').valueChanges.pipe(
takeUntil(this.destroy$)
@ -262,6 +351,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
this.timewindowForm.get('history.historyType').setValue(HistoryWindowType.INTERVAL);
}
}
this.updateDisableAdvancedOptionState('history.disableCustomInterval', hideLastInterval);
});
this.timewindowForm.get('history.hideFixedInterval').valueChanges.pipe(
takeUntil(this.destroy$)
@ -285,6 +375,13 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
}
}
});
this.timewindowForm.get('hideAggInterval').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value: boolean) => {
this.updateDisableAdvancedOptionState('realtime.disableCustomGroupInterval', value);
this.updateDisableAdvancedOptionState('history.disableCustomGroupInterval', value);
});
}
ngOnDestroy() {
@ -292,6 +389,15 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
this.destroy$.complete();
}
private updateDisableAdvancedOptionState(controlName: string, intervalHidden: boolean) {
if (intervalHidden) {
this.timewindowForm.get(controlName).disable({emitEvent: false});
this.timewindowForm.get(controlName).patchValue(false, {emitEvent: false});
} else {
this.timewindowForm.get(controlName).enable({emitEvent: false});
}
}
private updateValidators(aggType: AggregationType) {
if (aggType !== AggregationType.NONE) {
this.timewindowForm.get('aggregation.limit').clearValidators();
@ -303,34 +409,12 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
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));
}
const realtimeDisableCustomInterval = timewindowFormValue.realtime.disableCustomInterval;
const historyDisableCustomInterval = timewindowFormValue.history.disableCustomInterval;
updateFormValuesOnTimewindowTypeChange(selectedTab, this.quickIntervalOnly, this.timewindowForm,
realtimeDisableCustomInterval, historyDisableCustomInterval,
timewindowFormValue.realtime.advancedParams, timewindowFormValue.history.advancedParams);
this.timewindowForm.patchValue({
aggregation: {
type: timewindowFormValue.aggregation.type,
limit: timewindowFormValue.aggregation.limit
},
timezone: timewindowFormValue.timezone,
hideAggregation: timewindowFormValue.hideAggregation,
hideAggInterval: timewindowFormValue.hideAggInterval,
hideTimezone: timewindowFormValue.hideTimezone
@ -340,6 +424,75 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
update() {
const timewindowFormValue = this.timewindowForm.getRawValue();
this.timewindow = mergeDeep(this.timewindow, timewindowFormValue);
const realtimeConfigurableLastIntervalsAvailable = !(timewindowFormValue.hideAggInterval &&
(timewindowFormValue.realtime.hideInterval || timewindowFormValue.realtime.hideLastInterval));
const realtimeConfigurableQuickIntervalsAvailable = !(timewindowFormValue.hideAggInterval &&
(timewindowFormValue.realtime.hideInterval || timewindowFormValue.realtime.hideQuickInterval));
const historyConfigurableLastIntervalsAvailable = !(timewindowFormValue.hideAggInterval &&
(timewindowFormValue.history.hideInterval || timewindowFormValue.history.hideLastInterval));
const historyConfigurableQuickIntervalsAvailable = !(timewindowFormValue.hideAggInterval &&
(timewindowFormValue.history.hideInterval || timewindowFormValue.history.hideQuickInterval));
if (realtimeConfigurableLastIntervalsAvailable && timewindowFormValue.realtime.advancedParams.allowedLastIntervals?.length) {
this.timewindow.realtime.advancedParams.allowedLastIntervals = timewindowFormValue.realtime.advancedParams.allowedLastIntervals;
} else {
delete this.timewindow.realtime.advancedParams.allowedLastIntervals;
}
if (realtimeConfigurableQuickIntervalsAvailable && timewindowFormValue.realtime.advancedParams.allowedQuickIntervals?.length) {
this.timewindow.realtime.advancedParams.allowedQuickIntervals = timewindowFormValue.realtime.advancedParams.allowedQuickIntervals;
} else {
delete this.timewindow.realtime.advancedParams.allowedQuickIntervals;
}
if (realtimeConfigurableLastIntervalsAvailable && isObject(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig) &&
Object.keys(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig).length) {
this.timewindow.realtime.advancedParams.lastAggIntervalsConfig = timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig;
} else {
delete this.timewindow.realtime.advancedParams.lastAggIntervalsConfig;
}
if (realtimeConfigurableQuickIntervalsAvailable && isObject(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig) &&
Object.keys(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig).length) {
this.timewindow.realtime.advancedParams.quickAggIntervalsConfig = timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig;
} else {
delete this.timewindow.realtime.advancedParams.quickAggIntervalsConfig;
}
if (historyConfigurableLastIntervalsAvailable && timewindowFormValue.history.advancedParams.allowedLastIntervals?.length) {
this.timewindow.history.advancedParams.allowedLastIntervals = timewindowFormValue.history.advancedParams.allowedLastIntervals;
} else {
delete this.timewindow.history.advancedParams.allowedLastIntervals;
}
if (historyConfigurableQuickIntervalsAvailable && timewindowFormValue.history.advancedParams.allowedQuickIntervals?.length) {
this.timewindow.history.advancedParams.allowedQuickIntervals = timewindowFormValue.history.advancedParams.allowedQuickIntervals;
} else {
delete this.timewindow.history.advancedParams.allowedQuickIntervals;
}
if (historyConfigurableLastIntervalsAvailable && isObject(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig) &&
Object.keys(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig).length) {
this.timewindow.history.advancedParams.lastAggIntervalsConfig = timewindowFormValue.history.advancedParams.lastAggIntervalsConfig;
} else {
delete this.timewindow.history.advancedParams.lastAggIntervalsConfig;
}
if (historyConfigurableQuickIntervalsAvailable && isObject(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig) &&
Object.keys(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig).length) {
this.timewindow.history.advancedParams.quickAggIntervalsConfig = timewindowFormValue.history.advancedParams.quickAggIntervalsConfig;
} else {
delete this.timewindow.history.advancedParams.quickAggIntervalsConfig;
}
if (!Object.keys(this.timewindow.realtime.advancedParams).length) {
delete this.timewindow.realtime.advancedParams;
}
if (!Object.keys(this.timewindow.history.advancedParams).length) {
delete this.timewindow.history.advancedParams;
}
if (timewindowFormValue.allowedAggTypes?.length && !timewindowFormValue.hideAggregation) {
this.timewindow.allowedAggTypes = timewindowFormValue.allowedAggTypes;
} else {
delete this.timewindow.allowedAggTypes;
}
if (!this.timewindow.realtime.disableCustomInterval) {
delete this.timewindow.realtime.disableCustomInterval;
}
@ -352,6 +505,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
if (!this.timewindow.history.disableCustomGroupInterval) {
delete this.timewindow.history.disableCustomGroupInterval;
}
if (!this.aggregation) {
delete this.timewindow.aggregation;
}
@ -362,46 +516,123 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
this.dialogRef.close();
}
minRealtimeAggInterval() {
get minRealtimeAggInterval() {
return this.timeService.minIntervalLimit(this.currentRealtimeTimewindow());
}
maxRealtimeAggInterval() {
get 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;
}
private currentRealtimeTimewindow(): number {
return currentRealtimeTimewindow(this.timewindowForm.getRawValue());
}
minHistoryAggInterval() {
get minHistoryAggInterval() {
return this.timeService.minIntervalLimit(this.currentHistoryTimewindow());
}
maxHistoryAggInterval() {
get maxHistoryAggInterval() {
return this.timeService.maxIntervalLimit(this.currentHistoryTimewindow());
}
currentHistoryTimewindow() {
private currentHistoryTimewindow(): number {
return currentHistoryTimewindow(this.timewindowForm.getRawValue());
}
get realtimeAllowedAggIntervals(): Array<Interval> {
const timewindowFormValue = this.timewindowForm.getRawValue();
return realtimeAllowedAggIntervals(timewindowFormValue, timewindowFormValue.realtime.advancedParams);
}
get historyAllowedAggIntervals(): Array<Interval> {
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;
return historyAllowedAggIntervals(timewindowFormValue, timewindowFormValue.history.advancedParams);
}
openAggregationOptionsConfig($event: Event) {
if ($event) {
$event.stopPropagation();
}
const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
return DAY;
const aggregationConfigPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, AggregationOptionsConfigPanelComponent, ['left', 'leftTop', 'leftBottom'], true, null,
{
allowedAggregationTypes: deepClone(this.timewindowForm.get('allowedAggTypes').value),
onClose: (result: Array<AggregationType> | null) => {
aggregationConfigPopover.hide();
if (result) {
this.timewindowForm.get('allowedAggTypes').patchValue(result);
this.timewindowForm.markAsDirty();
}
}
},
{maxHeight: '500px', height: '100%'},
{}, {}, true, () => {}, {padding: 0});
aggregationConfigPopover.tbComponentRef.instance.popoverComponent = aggregationConfigPopover;
}
this.cd.detectChanges();
}
configureRealtimeLastIntervalOptions($event: Event) {
this.openIntervalOptionsConfig($event,
'realtime.advancedParams.allowedLastIntervals', 'realtime.advancedParams.lastAggIntervalsConfig',
RealtimeWindowType.LAST_INTERVAL);
}
configureRealtimeQuickIntervalOptions($event: Event) {
this.openIntervalOptionsConfig($event,
'realtime.advancedParams.allowedQuickIntervals', 'realtime.advancedParams.quickAggIntervalsConfig',
RealtimeWindowType.INTERVAL, TimewindowType.REALTIME);
}
configureHistoryLastIntervalOptions($event: Event) {
this.openIntervalOptionsConfig($event,
'history.advancedParams.allowedLastIntervals', 'history.advancedParams.lastAggIntervalsConfig',
HistoryWindowType.LAST_INTERVAL);
}
configureHistoryQuickIntervalOptions($event: Event) {
this.openIntervalOptionsConfig($event,
'history.advancedParams.allowedQuickIntervals', 'history.advancedParams.quickAggIntervalsConfig',
HistoryWindowType.INTERVAL, TimewindowType.HISTORY);
}
private openIntervalOptionsConfig($event: Event,
allowedIntervalsControlName: string,
aggIntervalsConfigControlName: string,
intervalType: RealtimeWindowType | HistoryWindowType, timewindowType?: TimewindowType) {
if ($event) {
$event.stopPropagation();
}
const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const intervalsConfigPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, IntervalOptionsConfigPanelComponent, ['left', 'leftTop', 'leftBottom'], true, null,
{
aggregation: this.aggregation,
allowedIntervals: deepClone(this.timewindowForm.get(allowedIntervalsControlName).value),
aggIntervalsConfig: deepClone(this.timewindowForm.get(aggIntervalsConfigControlName).value),
intervalType: intervalType,
timewindowType: timewindowType,
onClose: (result: IntervalOptionsConfigPanelData | null) => {
intervalsConfigPopover.hide();
if (result) {
this.timewindowForm.get(allowedIntervalsControlName).patchValue(result.allowedIntervals);
this.timewindowForm.get(aggIntervalsConfigControlName).patchValue(result.aggIntervalsConfig);
this.timewindowForm.markAsDirty();
}
}
},
{maxHeight: '500px', height: '100%'},
{}, {}, true, () => {}, {padding: 0});
intervalsConfigPopover.tbComponentRef.instance.popoverComponent = intervalsConfigPopover;
}
this.cd.detectChanges();
}
}

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

@ -20,17 +20,14 @@
.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 {
tb-datapoints-limit,
tb-aggregation-type-select {
flex: 1;
}
}

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

@ -51,7 +51,8 @@
formControlName="timewindowMs"
subscriptSizing="dynamic"
appearance="outline"
[disabledAdvanced]="timewindow.realtime.disableCustomInterval"
[allowedIntervals]="realtimeAllowedLastIntervals"
[disabledAdvanced]="realtimeDisableCustomInterval"
[required]="timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL">
</tb-timeinterval>
@ -63,6 +64,7 @@
onlyCurrentInterval="true"
subscriptSizing="dynamic"
appearance="outline"
[allowedIntervals]="realtimeAllowedQuickIntervals"
[required]="timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL">
</tb-quick-time-interval>
@ -95,7 +97,8 @@
formControlName="timewindowMs"
subscriptSizing="dynamic"
appearance="outline"
[disabledAdvanced]="timewindow.history.disableCustomInterval"
[allowedIntervals]="historyAllowedLastIntervals"
[disabledAdvanced]="historyDisableCustomInterval"
[required]="timewindowForm.get('selectedTab').value === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL">
</tb-timeinterval>
@ -116,6 +119,7 @@
formControlName="quickInterval"
subscriptSizing="dynamic"
appearance="outline"
[allowedIntervals]="historyAllowedQuickIntervals"
[required]="timewindowForm.get('selectedTab').value === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.INTERVAL">
</tb-quick-time-interval>
@ -127,13 +131,9 @@
<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>
<tb-aggregation-type-select class="flex" subscriptSizing="dynamic" appearance="outline" displayLabel="false"
formControlName="type" [allowedAggregationTypes]="allowedAggTypes">
</tb-aggregation-type-select>
</section>
<section class="tb-form-row column-xs space-between same-padding"
@ -145,27 +145,29 @@
</section>
</ng-container>
<section class="tb-form-row column-xs same-padding" [class.!hidden]="!(isEdit || !timewindow.hideAggInterval)
|| timewindowForm.get('aggregation.type').value === aggregationTypes.NONE">
<section class="tb-form-row column-xs same-padding" *ngIf="timewindowForm.get('aggregation.type').value !== aggregationTypes.NONE"
[class.!hidden]="!(isEdit || !timewindow.hideAggInterval)">
<ng-container formGroupName="realtime" *ngIf="timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME">
<tb-timeinterval
formControlName="interval"
[min]="minRealtimeAggInterval()" [max]="maxRealtimeAggInterval()"
[min]="minRealtimeAggInterval" [max]="maxRealtimeAggInterval"
[allowedIntervals]="realtimeAllowedAggIntervals"
useCalendarIntervals
subscriptSizing="dynamic"
appearance="outline"
[disabledAdvanced]="timewindow.realtime.disableCustomGroupInterval">
[disabledAdvanced]="realtimeDisableCustomGroupInterval">
<div class="fixed-title-width-180">{{ 'aggregation.group-interval' | translate }}</div>
</tb-timeinterval>
</ng-container>
<ng-container formGroupName="history" *ngIf="timewindowForm.get('selectedTab').value === timewindowTypes.HISTORY">
<tb-timeinterval
formControlName="interval"
[min]="minHistoryAggInterval()" [max]="maxHistoryAggInterval()"
[min]="minHistoryAggInterval" [max]="maxHistoryAggInterval"
[allowedIntervals]="historyAllowedAggIntervals"
useCalendarIntervals
subscriptSizing="dynamic"
appearance="outline"
[disabledAdvanced]="timewindow.history.disableCustomGroupInterval">
[disabledAdvanced]="historyDisableCustomGroupInterval">
<div class="fixed-title-width-180">{{ 'aggregation.group-interval' | translate }}</div>
</tb-timeinterval>
</ng-container>

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

@ -27,6 +27,12 @@
align-items: center;
}
&-content {
.tb-flex {
gap: 16px;
}
}
&-type-options {
flex: 1;
}

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

@ -16,16 +16,21 @@
import { Component, Inject, InjectionToken, OnDestroy, OnInit, ViewContainerRef } from '@angular/core';
import {
aggregationTranslations,
AggregationType,
DAY,
currentHistoryTimewindow,
currentRealtimeTimewindow,
historyAllowedAggIntervals,
HistoryWindowType,
historyWindowTypeTranslations,
quickTimeIntervalPeriod,
Interval,
QuickTimeInterval,
realtimeAllowedAggIntervals,
RealtimeWindowType,
realtimeWindowTypeTranslations,
Timewindow,
TimewindowType
TimewindowAdvancedParams,
TimewindowType,
updateFormValuesOnTimewindowTypeChange
} from '@shared/models/time/time.models';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
@ -87,10 +92,6 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
aggregationTypes = AggregationType;
aggregations = Object.keys(AggregationType);
aggregationTypesTranslations = aggregationTranslations;
result: Timewindow;
timewindowTypeOptions: ToggleHeaderOption[] = [{
@ -108,6 +109,19 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
historyIntervalSelectionAvailable: boolean;
aggregationOptionsAvailable: boolean;
realtimeDisableCustomInterval: boolean;
realtimeDisableCustomGroupInterval: boolean;
historyDisableCustomInterval: boolean;
historyDisableCustomGroupInterval: boolean;
realtimeAdvancedParams: TimewindowAdvancedParams;
realtimeAllowedLastIntervals: Array<Interval>;
realtimeAllowedQuickIntervals: Array<QuickTimeInterval>;
historyAdvancedParams: TimewindowAdvancedParams;
historyAllowedLastIntervals: Array<Interval>;
historyAllowedQuickIntervals: Array<QuickTimeInterval>;
allowedAggTypes: Array<AggregationType>;
private destroy$ = new Subject<void>();
constructor(@Inject(TIMEWINDOW_PANEL_DATA) public data: TimewindowPanelData,
@ -127,6 +141,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
this.timezone = data.timezone;
this.isEdit = data.isEdit;
this.updateTimewindowAdvancedParams();
if (!this.historyOnly) {
this.timewindowTypeOptions.unshift({
name: this.translate.instant('timewindow.realtime'),
@ -285,11 +301,55 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
}]
});
this.updateValidators(this.timewindowForm.get('aggregation.type').value);
this.timewindowForm.get('aggregation.type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((aggregationType: AggregationType) => {
this.updateValidators(aggregationType);
});
if (this.aggregation) {
this.timewindowForm.get('realtime.timewindowMs').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((timewindowMs: number) => {
if (this.realtimeAdvancedParams?.lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) &&
this.realtimeAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval) {
this.timewindowForm.get('realtime.interval').patchValue(
this.realtimeAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false}
);
}
});
this.timewindowForm.get('realtime.quickInterval').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((quickInterval: number) => {
if (this.realtimeAdvancedParams?.quickAggIntervalsConfig?.hasOwnProperty(quickInterval) &&
this.realtimeAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval) {
this.timewindowForm.get('realtime.interval').patchValue(
this.realtimeAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval, {emitEvent: false}
);
}
});
this.timewindowForm.get('history.timewindowMs').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((timewindowMs: number) => {
if (this.historyAdvancedParams?.lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) &&
this.historyAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval) {
this.timewindowForm.get('history.interval').patchValue(
this.historyAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false}
);
}
});
this.timewindowForm.get('history.quickInterval').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((quickInterval: number) => {
if (this.historyAdvancedParams?.quickAggIntervalsConfig?.hasOwnProperty(quickInterval) &&
this.historyAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval) {
this.timewindowForm.get('history.interval').patchValue(
this.historyAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval, {emitEvent: false}
);
}
});
this.timewindowForm.get('aggregation.type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((aggregationType: AggregationType) => {
this.updateValidators(aggregationType);
});
}
this.timewindowForm.get('selectedTab').valueChanges.pipe(
takeUntil(this.destroy$)
@ -313,40 +373,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
}
private onTimewindowTypeChange(selectedTab: TimewindowType) {
const timewindowFormValue = this.timewindowForm.getRawValue();
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]] :
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 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,
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
});
updateFormValuesOnTimewindowTypeChange(selectedTab, this.quickIntervalOnly, this.timewindowForm,
this.realtimeDisableCustomInterval, this.historyDisableCustomInterval,
this.realtimeAdvancedParams, this.historyAdvancedParams);
}
update() {
@ -371,6 +400,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
fixedTimewindow: timewindowFormValue.history.fixedTimewindow,
quickInterval: timewindowFormValue.history.quickInterval,
}};
if (this.aggregation) {
this.timewindow.aggregation = {
type: timewindowFormValue.aggregation.type,
@ -455,46 +485,36 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
this.overlayRef.dispose();
}
minRealtimeAggInterval() {
get minRealtimeAggInterval() {
return this.timeService.minIntervalLimit(this.currentRealtimeTimewindow());
}
maxRealtimeAggInterval() {
get 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;
}
private currentRealtimeTimewindow(): number {
return currentRealtimeTimewindow(this.timewindowForm.getRawValue());
}
minHistoryAggInterval() {
get minHistoryAggInterval() {
return this.timeService.minIntervalLimit(this.currentHistoryTimewindow());
}
maxHistoryAggInterval() {
get 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;
}
private currentHistoryTimewindow(): number {
return currentHistoryTimewindow(this.timewindowForm.getRawValue());
}
get realtimeAllowedAggIntervals(): Array<Interval> {
return realtimeAllowedAggIntervals(this.timewindowForm.getRawValue(), this.realtimeAdvancedParams);
}
get historyAllowedAggIntervals(): Array<Interval> {
return historyAllowedAggIntervals(this.timewindowForm.getRawValue(), this.historyAdvancedParams);
}
openTimewindowConfig() {
@ -513,8 +533,37 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
.subscribe((res) => {
if (res) {
this.timewindow = res;
this.updateTimewindowAdvancedParams();
this.updateTimewindowForm();
}
});
}
private updateTimewindowAdvancedParams() {
this.realtimeDisableCustomInterval = this.timewindow.realtime.disableCustomInterval;
this.realtimeDisableCustomGroupInterval = this.timewindow.realtime.disableCustomGroupInterval;
this.historyDisableCustomInterval = this.timewindow.history.disableCustomInterval;
this.historyDisableCustomGroupInterval = this.timewindow.history.disableCustomGroupInterval;
if (this.timewindow.realtime.advancedParams) {
this.realtimeAdvancedParams = this.timewindow.realtime.advancedParams;
this.realtimeAllowedLastIntervals = this.timewindow.realtime.advancedParams.allowedLastIntervals;
this.realtimeAllowedQuickIntervals = this.timewindow.realtime.advancedParams.allowedQuickIntervals;
} else {
this.realtimeAdvancedParams = null;
this.realtimeAllowedLastIntervals = null;
this.realtimeAllowedQuickIntervals = null;
}
if (this.timewindow.history.advancedParams) {
this.historyAdvancedParams = this.timewindow.history.advancedParams;
this.historyAllowedLastIntervals = this.timewindow.history.advancedParams.allowedLastIntervals;
this.historyAllowedQuickIntervals = this.timewindow.history.advancedParams.allowedQuickIntervals;
} else {
this.historyAdvancedParams = null;
this.historyAllowedLastIntervals = null;
this.historyAllowedQuickIntervals = null;
}
this.allowedAggTypes = this.timewindow.allowedAggTypes;
}
}

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

@ -19,6 +19,7 @@ import { deepClone, isDefined, isDefinedAndNotNull, isNumeric, isUndefined } fro
import moment_ from 'moment';
import * as momentTz from 'moment-timezone';
import { IntervalType } from '@shared/models/telemetry/telemetry.models';
import { FormGroup } from '@angular/forms';
const moment = moment_;
@ -84,6 +85,24 @@ export class IntervalMath {
}
}
export interface TimewindowAdvancedParams {
allowedLastIntervals? : Array<Interval>;
allowedQuickIntervals? : Array<QuickTimeInterval>;
lastAggIntervalsConfig? : TimewindowAggIntervalsConfig;
quickAggIntervalsConfig? : TimewindowAggIntervalsConfig;
}
export type TimewindowInterval = Interval | QuickTimeInterval;
export interface TimewindowAggIntervalsConfig {
[key: string]: TimewindowAggIntervalOptions;
}
export interface TimewindowAggIntervalOptions {
aggIntervals?: Array<Interval>;
defaultAggInterval?: Interval;
}
export interface IntervalWindow {
interval?: Interval;
timewindowMs?: number;
@ -94,6 +113,7 @@ export interface IntervalWindow {
hideLastInterval?: boolean;
hideQuickInterval?: boolean;
hideFixedInterval?: boolean;
advancedParams?: TimewindowAdvancedParams
}
export interface RealtimeWindow extends IntervalWindow{
@ -139,6 +159,7 @@ export interface Aggregation {
export interface Timewindow {
displayValue?: string;
displayTimezoneAbbr?: string;
allowedAggTypes?: Array<AggregationType>;
hideAggregation?: boolean;
hideAggInterval?: boolean;
hideTimezone?: boolean;
@ -175,6 +196,12 @@ export interface WidgetTimewindow {
stDiff?: number;
}
export interface TimewindowIntervalOption {
name: string;
translateParams?: {[key: string]: any};
value: TimewindowInterval;
}
export enum QuickTimeInterval {
YESTERDAY = 'YESTERDAY',
DAY_BEFORE_YESTERDAY = 'DAY_BEFORE_YESTERDAY',
@ -301,6 +328,9 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO
historyOnly: boolean, timeService: TimeService): Timewindow => {
const model = defaultTimewindow(timeService);
if (value) {
if (value.allowedAggTypes?.length) {
model.allowedAggTypes = value.allowedAggTypes;
}
model.hideAggregation = value.hideAggregation;
model.hideAggInterval = value.hideAggInterval;
model.hideTimezone = value.hideTimezone;
@ -356,6 +386,10 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO
if (isDefined(value.realtime.timewindowMs)) {
model.realtime.timewindowMs = value.realtime.timewindowMs;
}
if (value.realtime.advancedParams) {
model.realtime.advancedParams = value.realtime.advancedParams;
}
}
if (isDefined(value.history)) {
if (isDefinedAndNotNull(value.history.hideInterval)) {
@ -405,6 +439,10 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO
model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs;
}
}
if (value.history.advancedParams) {
model.history.advancedParams = value.history.advancedParams;
}
}
if (value.aggregation) {
if (value.aggregation.type) {
@ -469,6 +507,12 @@ export const toHistoryTimewindow = (timewindow: Timewindow, startTimeMs: number,
if (timewindow.history?.disableCustomGroupInterval) {
historyTimewindow.history.disableCustomGroupInterval = timewindow.history.disableCustomGroupInterval;
}
if (timewindow.history?.advancedParams) {
historyTimewindow.history.advancedParams = timewindow.history.advancedParams;
}
if (timewindow.allowedAggTypes?.length) {
historyTimewindow.allowedAggTypes = timewindow.allowedAggTypes;
}
return historyTimewindow;
};
@ -481,6 +525,140 @@ export const timewindowTypeChanged = (newTimewindow: Timewindow, oldTimewindow:
return newType !== oldType;
};
export const updateFormValuesOnTimewindowTypeChange = (selectedTab: TimewindowType,
quickIntervalOnly: boolean, timewindowForm: FormGroup,
realtimeDisableCustomInterval: boolean, historyDisableCustomInterval: boolean,
realtimeAdvancedParams?: TimewindowAdvancedParams,
historyAdvancedParams?: TimewindowAdvancedParams) => {
const timewindowFormValue = timewindowForm.getRawValue();
if (selectedTab === TimewindowType.REALTIME) {
if (timewindowFormValue.history.historyType !== HistoryWindowType.FIXED
&& !(quickIntervalOnly && timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL)) {
if (Object.keys(RealtimeWindowType).includes(HistoryWindowType[timewindowFormValue.history.historyType])) {
timewindowForm.get('realtime.realtimeType').patchValue(RealtimeWindowType[HistoryWindowType[timewindowFormValue.history.historyType]]);
}
if (!realtimeDisableCustomInterval ||
!realtimeAdvancedParams?.allowedLastIntervals?.length || realtimeAdvancedParams.allowedLastIntervals.includes(timewindowFormValue.history.timewindowMs)) {
timewindowForm.get('realtime.timewindowMs').patchValue(timewindowFormValue.history.timewindowMs);
}
if (realtimeAdvancedParams?.allowedQuickIntervals?.includes(timewindowFormValue.history.quickInterval) ||
(!realtimeAdvancedParams?.allowedQuickIntervals?.length && timewindowFormValue.history.quickInterval.startsWith('CURRENT'))) {
timewindowForm.get('realtime.quickInterval').patchValue(timewindowFormValue.history.quickInterval);
}
const defaultAggInterval = realtimeDefaultAggInterval(timewindowForm.getRawValue(), realtimeAdvancedParams);
const allowedAggIntervals = realtimeAllowedAggIntervals(timewindowForm.getRawValue(), realtimeAdvancedParams);
if (defaultAggInterval || !allowedAggIntervals.length || allowedAggIntervals.includes(timewindowFormValue.history.interval)) {
setTimeout(() => timewindowForm.get('realtime.interval').patchValue(
defaultAggInterval ?? timewindowFormValue.history.interval
));
}
}
} else {
timewindowForm.get('history.historyType').patchValue(HistoryWindowType[RealtimeWindowType[timewindowFormValue.realtime.realtimeType]]);
if (!historyDisableCustomInterval ||
!historyAdvancedParams?.allowedLastIntervals?.length || historyAdvancedParams.allowedLastIntervals?.includes(timewindowFormValue.realtime.timewindowMs)) {
timewindowForm.get('history.timewindowMs').patchValue(timewindowFormValue.realtime.timewindowMs);
}
if (!historyAdvancedParams?.allowedQuickIntervals?.length || historyAdvancedParams.allowedQuickIntervals?.includes(timewindowFormValue.realtime.quickInterval)) {
timewindowForm.get('history.quickInterval').patchValue(timewindowFormValue.realtime.quickInterval);
}
const defaultAggInterval = historyDefaultAggInterval(timewindowForm.getRawValue(), historyAdvancedParams);
const allowedAggIntervals = historyAllowedAggIntervals(timewindowForm.getRawValue(), historyAdvancedParams);
if (defaultAggInterval || !allowedAggIntervals.length || allowedAggIntervals.includes(timewindowFormValue.realtime.interval)) {
setTimeout(() => timewindowForm.get('history.interval').patchValue(
defaultAggInterval ?? timewindowFormValue.realtime.interval
));
}
}
timewindowForm.patchValue({
aggregation: {
type: timewindowFormValue.aggregation.type,
limit: timewindowFormValue.aggregation.limit
},
timezone: timewindowFormValue.timezone
});
};
export const currentRealtimeTimewindow = (timewindow: Timewindow): number => {
switch (timewindow.realtime.realtimeType) {
case RealtimeWindowType.LAST_INTERVAL:
return timewindow.realtime.timewindowMs;
case RealtimeWindowType.INTERVAL:
return quickTimeIntervalPeriod(timewindow.realtime.quickInterval);
default:
return DAY;
}
};
export const currentHistoryTimewindow = (timewindow: Timewindow): number => {
if (timewindow.history.historyType === HistoryWindowType.LAST_INTERVAL) {
return timewindow.history.timewindowMs;
} else if (timewindow.history.historyType === HistoryWindowType.INTERVAL) {
return quickTimeIntervalPeriod(timewindow.history.quickInterval);
} else if (timewindow.history.fixedTimewindow) {
return timewindow.history.fixedTimewindow.endTimeMs -
timewindow.history.fixedTimewindow.startTimeMs;
} else {
return DAY;
}
}
export const realtimeAllowedAggIntervals = (timewindow: Timewindow,
advancedParams: TimewindowAdvancedParams): Array<Interval> => {
if (timewindow.realtime.realtimeType === RealtimeWindowType.LAST_INTERVAL &&
advancedParams?.lastAggIntervalsConfig?.hasOwnProperty(timewindow.realtime.timewindowMs) &&
advancedParams.lastAggIntervalsConfig[timewindow.realtime.timewindowMs].aggIntervals?.length) {
return advancedParams.lastAggIntervalsConfig[timewindow.realtime.timewindowMs].aggIntervals;
} else if (timewindow.realtime.realtimeType === RealtimeWindowType.INTERVAL &&
advancedParams?.quickAggIntervalsConfig?.hasOwnProperty(timewindow.realtime.quickInterval) &&
advancedParams.quickAggIntervalsConfig[timewindow.realtime.quickInterval].aggIntervals?.length) {
return advancedParams.quickAggIntervalsConfig[timewindow.realtime.quickInterval].aggIntervals;
}
return [];
};
export const historyAllowedAggIntervals = (timewindow: Timewindow,
advancedParams: TimewindowAdvancedParams): Array<Interval> => {
if (timewindow.history.historyType === HistoryWindowType.LAST_INTERVAL &&
advancedParams?.lastAggIntervalsConfig?.hasOwnProperty(timewindow.history.timewindowMs) &&
advancedParams.lastAggIntervalsConfig[timewindow.history.timewindowMs].aggIntervals?.length) {
return advancedParams.lastAggIntervalsConfig[timewindow.history.timewindowMs].aggIntervals;
} else if (timewindow.history.historyType === HistoryWindowType.INTERVAL &&
advancedParams?.quickAggIntervalsConfig?.hasOwnProperty(timewindow.history.quickInterval) &&
advancedParams.quickAggIntervalsConfig[timewindow.history.quickInterval].aggIntervals?.length) {
return advancedParams.quickAggIntervalsConfig[timewindow.history.quickInterval].aggIntervals;
}
return [];
};
export const realtimeDefaultAggInterval = (timewindow: Timewindow,
advancedParams: TimewindowAdvancedParams): Interval => {
if (timewindow.realtime.realtimeType === RealtimeWindowType.LAST_INTERVAL &&
advancedParams?.lastAggIntervalsConfig?.hasOwnProperty(timewindow.realtime.timewindowMs) &&
advancedParams.lastAggIntervalsConfig[timewindow.realtime.timewindowMs].defaultAggInterval) {
return advancedParams.lastAggIntervalsConfig[timewindow.realtime.timewindowMs].defaultAggInterval;
} else if (timewindow.realtime.realtimeType === RealtimeWindowType.INTERVAL &&
advancedParams?.quickAggIntervalsConfig?.hasOwnProperty(timewindow.realtime.quickInterval) &&
advancedParams.quickAggIntervalsConfig[timewindow.realtime.quickInterval].defaultAggInterval) {
return advancedParams.quickAggIntervalsConfig[timewindow.realtime.quickInterval].defaultAggInterval;
}
return null;
};
export const historyDefaultAggInterval = (timewindow: Timewindow,
advancedParams: TimewindowAdvancedParams): Interval => {
if (timewindow.history.historyType === HistoryWindowType.LAST_INTERVAL &&
advancedParams?.lastAggIntervalsConfig?.hasOwnProperty(timewindow.history.timewindowMs) &&
advancedParams.lastAggIntervalsConfig[timewindow.history.timewindowMs].defaultAggInterval) {
return advancedParams.lastAggIntervalsConfig[timewindow.history.timewindowMs].defaultAggInterval;
} else if (timewindow.history.historyType === HistoryWindowType.INTERVAL &&
advancedParams?.quickAggIntervalsConfig?.hasOwnProperty(timewindow.history.quickInterval) &&
advancedParams.quickAggIntervalsConfig[timewindow.history.quickInterval].defaultAggInterval) {
return advancedParams.quickAggIntervalsConfig[timewindow.history.quickInterval].defaultAggInterval;
}
return null;
};
export const getTimezone = (tz: string): moment_.Moment => moment.tz(tz);
export const calculateTsOffset = (timezone?: string): number => {
@ -917,6 +1095,9 @@ export const createTimewindowForComparison = (subscriptionTimewindow: Subscripti
export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => {
const cloned: Timewindow = {};
if (timewindow.allowedAggTypes?.length) {
cloned.allowedAggTypes = timewindow.allowedAggTypes;
}
cloned.hideAggregation = timewindow.hideAggregation || false;
cloned.hideAggInterval = timewindow.hideAggInterval || false;
cloned.hideTimezone = timewindow.hideTimezone || false;
@ -1054,6 +1235,10 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
}
);
export const intervalValuesToTimeIntervals = (intervalValues: Array<Interval>): Array<TimeInterval> => {
return defaultTimeIntervals.filter(interval => intervalValues.includes(interval.value));
}
export enum TimeUnit {
SECONDS = 'SECONDS',
MINUTES = 'MINUTES',

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

@ -218,6 +218,10 @@ import { CountryAutocompleteComponent } from '@shared/components/country-autocom
import { CountryData } from '@shared/models/country.models';
import { SvgXmlComponent } from '@shared/components/svg-xml.component';
import { DatapointsLimitComponent } from '@shared/components/time/datapoints-limit.component';
import { AggregationTypeSelectComponent } from '@shared/components/time/aggregation/aggregation-type-select.component';
import { AggregationOptionsConfigPanelComponent } from '@shared/components/time/aggregation/aggregation-options-config-panel.component';
import { IntervalOptionsConfigPanelComponent } from '@shared/components/time/interval-options-config-panel.component';
import { GroupingIntervalOptionsComponent } from '@shared/components/time/aggregation/grouping-interval-options.component';
import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component';
import { JsFuncModuleRowComponent } from '@shared/components/js-func-module-row.component';
@ -305,10 +309,14 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
TimewindowPanelComponent,
TimewindowConfigDialogComponent,
TimeintervalComponent,
GroupingIntervalOptionsComponent,
TimezoneComponent,
TimezonePanelComponent,
QuickTimeIntervalComponent,
DatapointsLimitComponent,
AggregationTypeSelectComponent,
AggregationOptionsConfigPanelComponent,
IntervalOptionsConfigPanelComponent,
DashboardSelectComponent,
DashboardSelectPanelComponent,
DatetimePeriodComponent,
@ -514,10 +522,14 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
TimewindowPanelComponent,
TimewindowConfigDialogComponent,
TimeintervalComponent,
GroupingIntervalOptionsComponent,
TimezoneComponent,
TimezonePanelComponent,
QuickTimeIntervalComponent,
DatapointsLimitComponent,
AggregationTypeSelectComponent,
AggregationOptionsConfigPanelComponent,
IntervalOptionsConfigPanelComponent,
DashboardSelectComponent,
DatetimePeriodComponent,
DatetimeComponent,

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

@ -80,7 +80,8 @@
"clear": "Clear",
"upload": "Upload",
"delete-anyway": "Delete anyway",
"delete-selected": "Delete selected"
"delete-selected": "Delete selected",
"set": "Set"
},
"aggregation": {
"aggregation": "Aggregation",
@ -4778,7 +4779,16 @@
"hide-group-interval": "Hide grouping interval from end-users",
"hide-max-values": "Hide max values from end-users",
"hide-timezone": "Hide time zone from end-users",
"disable-custom-interval": "Disable custom interval selection"
"disable-custom-interval": "Disable custom interval selection",
"edit-aggregation-functions-list": "Edit aggregation functions list",
"edit-aggregation-functions-list-hint": "List of available options can be specified.",
"allowed-aggregation-functions": "Allowed aggregation functions",
"edit-intervals-list": "Edit intervals list",
"allowed-agg-intervals": "Grouping intervals",
"default-agg-interval": "Default grouping interval",
"edit-intervals-list-hint": "List of available interval options can be specified.",
"edit-grouping-intervals-list-hint": "It is possible to configure the grouping intervals list and default grouping interval.",
"all": "All"
},
"tooltip": {
"trigger": "Trigger",

Loading…
Cancel
Save