diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index a9f2dc4cec..0cc3f5bb2f 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/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, diff --git a/ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.html b/ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.html new file mode 100644 index 0000000000..c626c8d615 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.html @@ -0,0 +1,47 @@ + +
+
{{ 'timewindow.edit-aggregation-functions-list' | translate }}
+
{{ 'timewindow.edit-aggregation-functions-list-hint' | translate }}
+
+
+
{{"timewindow.allowed-aggregation-functions" | translate }}
+
+
+ + + {{ aggregationTypesTranslations.get(aggregationTypes[type]) | translate }} + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.scss b/ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.scss new file mode 100644 index 0000000000..903097ba6b --- /dev/null +++ b/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; + } + } + } +} + diff --git a/ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.ts b/ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.ts new file mode 100644 index 0000000000..c206f712b9 --- /dev/null +++ b/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; + + @Input() + onClose: (result: Array | null) => void; + + @Input() + popoverComponent: TbPopoverComponent; + + aggregationOptionsConfigForm: FormGroup; + + aggregationTypes = AggregationType; + + allAggregationTypes: Array = 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); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.html b/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.html new file mode 100644 index 0000000000..205e25cdf5 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.html @@ -0,0 +1,30 @@ + + + {{ label }} + + + {{ displayAggregationTypeFn(type) }} + + + + + + diff --git a/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.scss b/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.scss new file mode 100644 index 0000000000..9cb0a0ea09 --- /dev/null +++ b/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; + } + } +} diff --git a/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.ts b/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.ts new file mode 100644 index 0000000000..a746132bc3 --- /dev/null +++ b/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; + + @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; + + private defaultLabel = this.translate.instant('aggregation.aggregation'); + + private allAggregationTypes: Array = 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 ''; + } + } + +} diff --git a/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.html b/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.html new file mode 100644 index 0000000000..9854427ba6 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.html @@ -0,0 +1,41 @@ + +
+ + + + {{ 'timewindow.all' | translate }} + + + {{ interval.name | translate:interval.translateParams }} + + + + + + -- + + {{ interval.name | translate:interval.translateParams }} + + + +
diff --git a/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.scss b/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.scss new file mode 100644 index 0000000000..15fa21f554 --- /dev/null +++ b/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%; + } +} + diff --git a/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.ts b/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.ts new file mode 100644 index 0000000000..304e885995 --- /dev/null +++ b/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; + allIntervalValues: Array; + selectedIntervals: Array; + + 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) { + const selectedIntervals = !intervals?.length ? this.allIntervalValues : intervals; + this.timeintervalFormGroup.get('aggIntervals').patchValue( + selectedIntervals, + {emitEvent: false}); + this.setSelectedIntervals(selectedIntervals); + } + + private setSelectedIntervals(selectedIntervalValues: Array) { + 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; + const intervals: Array = 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); + } + +} diff --git a/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.html b/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.html new file mode 100644 index 0000000000..c32afb983a --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.html @@ -0,0 +1,85 @@ + +
+
{{ 'timewindow.edit-intervals-list' | translate }}
+
+ {{ 'timewindow.edit-intervals-list-hint' | translate }} {{ aggregation ? ('timewindow.edit-grouping-intervals-list-hint' | translate) : '' }} +
+
+
+
+ + + {{ "timewindow.interval" | translate }} +
+ +
{{"timewindow.allowed-agg-intervals" | translate }}
+
{{"timewindow.default-agg-interval" | translate }}
+
+
+
+ +
+
+ + {{ interval.get('name').value }} + +
+ +
+ + +
+
+
+
+
+
+
+ + + + +
+
diff --git a/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.scss b/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.scss new file mode 100644 index 0000000000..26747f9006 --- /dev/null +++ b/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; + } + } + } + } + } +} + diff --git a/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.ts b/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.ts new file mode 100644 index 0000000000..38e96d3f87 --- /dev/null +++ b/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; + 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; + + @Input() + aggIntervalsConfig: TimewindowAggIntervalsConfig; + + @Input() + intervalType: RealtimeWindowType | HistoryWindowType; + + @Input() + timewindowType: TimewindowType; + + @Input() + onClose: (result: IntervalOptionsConfigPanelData | null) => void; + + @Input() + popoverComponent: TbPopoverComponent; + + intervalOptionsConfigForm: FormGroup; + + allIntervals: Array; + allIntervalValues: Array + + 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 = []; + 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 = []; + 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 { + 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); + } + +} diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html index 40f39118c8..b7cb004a75 100644 --- a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html +++ b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html @@ -15,13 +15,16 @@ limitations under the License. --> -
+
timewindow.interval - + {{ timeIntervalTranslationMap.get(interval) | translate}} + + +
diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss index de583225fb..e9b79fbf4c 100644 --- a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss +++ b/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; + } + } + } diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts index 87b3687d0e..2117ed6f67 100644 --- a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts +++ b/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 + @Input() subscriptSizing: SubscriptSizing = 'fixed'; @Input() appearance: MatFormFieldAppearance = 'fill'; + intervals: Array; + + private allAvailableIntervals: Array; + + 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; } } diff --git a/ui-ngx/src/app/shared/components/time/timeinterval.component.html b/ui-ngx/src/app/shared/components/time/timeinterval.component.html index 57e6ca0808..e9914c6068 100644 --- a/ui-ngx/src/app/shared/components/time/timeinterval.component.html +++ b/ui-ngx/src/app/shared/components/time/timeinterval.component.html @@ -25,6 +25,9 @@ {{ interval.name | translate:interval.translateParams }} + + +
; @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); diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html index e987da97ee..3574aff0b1 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html @@ -51,7 +51,7 @@ {{ 'timewindow.disable-custom-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"> +
+ class="tb-form-row column-xs">
@@ -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"> +
@@ -107,7 +121,7 @@ {{ 'timewindow.disable-custom-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"> +
-
+
{{ 'timewindow.hide' | translate }} @@ -140,7 +161,7 @@
-
+
{{ '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"> +
@@ -167,13 +195,15 @@
- - - - {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} - - - + + +
@@ -210,7 +240,8 @@ , 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 { + const timewindowFormValue = this.timewindowForm.getRawValue(); + return realtimeAllowedAggIntervals(timewindowFormValue, timewindowFormValue.realtime.advancedParams); + } + + get historyAllowedAggIntervals(): Array { 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 | 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(); + } } diff --git a/ui-ngx/src/app/shared/components/time/timewindow-form.scss b/ui-ngx/src/app/shared/components/time/timewindow-form.scss index 5f436acc09..19bd22da8f 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-form.scss +++ b/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; } } diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html index 60512f1f6b..d59a206d73 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html +++ b/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"> @@ -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"> @@ -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"> @@ -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"> @@ -127,13 +131,9 @@
{{ 'aggregation.aggregation' | translate }}
- - - - {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} - - - + +
-
+
+ [disabledAdvanced]="realtimeDisableCustomGroupInterval">
{{ 'aggregation.group-interval' | translate }}
+ [disabledAdvanced]="historyDisableCustomGroupInterval">
{{ 'aggregation.group-interval' | translate }}
diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss index dcf03ed094..f50b8c9938 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss +++ b/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; } diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index 844815f51a..14247997ac 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/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; + realtimeAllowedQuickIntervals: Array; + historyAdvancedParams: TimewindowAdvancedParams; + historyAllowedLastIntervals: Array; + historyAllowedQuickIntervals: Array; + allowedAggTypes: Array; + private destroy$ = new Subject(); 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 { + return realtimeAllowedAggIntervals(this.timewindowForm.getRawValue(), this.realtimeAdvancedParams); + } + + get historyAllowedAggIntervals(): Array { + 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; + } + } diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index bba91c5584..bf96f9de52 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/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; + allowedQuickIntervals? : Array; + lastAggIntervalsConfig? : TimewindowAggIntervalsConfig; + quickAggIntervalsConfig? : TimewindowAggIntervalsConfig; +} + +export type TimewindowInterval = Interval | QuickTimeInterval; + +export interface TimewindowAggIntervalsConfig { + [key: string]: TimewindowAggIntervalOptions; +} + +export interface TimewindowAggIntervalOptions { + aggIntervals?: Array; + 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; 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 => { + 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 => { + 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( } ); +export const intervalValuesToTimeIntervals = (intervalValues: Array): Array => { + return defaultTimeIntervals.filter(interval => intervalValues.includes(interval.value)); +} + export enum TimeUnit { SECONDS = 'SECONDS', MINUTES = 'MINUTES', diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index e142d2c541..560c0151c0 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/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, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index f9bba1a128..aa21a9c0c3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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",