Browse Source

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

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

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

@ -331,11 +331,14 @@ import * as AssetProfileComponent from '@home/components/profile/asset-profile.c
import * as AssetProfileDialogComponent from '@home/components/profile/asset-profile-dialog.component'; import * as AssetProfileDialogComponent from '@home/components/profile/asset-profile-dialog.component';
import * as AssetProfileAutocompleteComponent from '@home/components/profile/asset-profile-autocomplete.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 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 { 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 { Observable, map, of } from 'rxjs';
import { getFlexLayout } from '@shared/legacy/flex-layout.models'; import { getFlexLayout } from '@shared/legacy/flex-layout.models';
import { isJSResourceUrl } from '@shared/public-api'; 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-select.component': TimezoneSelectComponent,
'@shared/components/time/timezone.component': TimezoneComponent, '@shared/components/time/timezone.component': TimezoneComponent,
'@shared/components/time/timezone-panel.component': TimezonePanelComponent, '@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/value-input.component': ValueInputComponent,
'@shared/components/dashboard-autocomplete.component': DashboardAutocompleteComponent, '@shared/components/dashboard-autocomplete.component': DashboardAutocompleteComponent,
'@shared/components/entity/entity-subtype-autocomplete.component': EntitySubTypeAutocompleteComponent, '@shared/components/entity/entity-subtype-autocomplete.component': EntitySubTypeAutocompleteComponent,

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

@ -0,0 +1,47 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="aggregationOptionsConfigForm" class="tb-aggregation-options-form tb-form-panel no-border">
<div class="tb-form-panel-title">{{ 'timewindow.edit-aggregation-functions-list' | translate }}</div>
<div class="tb-form-hint tb-primary-fill">{{ 'timewindow.edit-aggregation-functions-list-hint' | translate }}</div>
<div class="tb-form-table no-gap no-padding">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell">{{"timewindow.allowed-aggregation-functions" | translate }}</div>
</div>
<div class="tb-form-table-body">
<mat-selection-list formControlName="allowedAggregationTypes">
<mat-list-option *ngFor="let type of allAggregationTypes" [value]="type" togglePosition="before" color="primary">
{{ aggregationTypesTranslations.get(aggregationTypes[type]) | translate }}
</mat-list-option>
</mat-selection-list>
</div>
</div>
<div class="tb-flex flex-end no-gap">
<button type="button"
mat-button
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="button"
mat-raised-button
color="primary"
(click)="update()"
[disabled]="aggregationOptionsConfigForm.invalid || !aggregationOptionsConfigForm.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</form>

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

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-aggregation-options-form {
height: 100%;
max-width: 350px;
.tb-form-table {
overflow: hidden;
}
.tb-form-table-body {
overflow-y: auto;
}
.tb-form-hint {
flex-shrink: 0;
}
.mdc-list {
display: flex;
flex-direction: column;
gap: 8px;
.mat-mdc-list-item.mdc-list-item--with-leading-checkbox.mdc-list-item--with-one-line {
height: 40px;
}
}
}
}

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

@ -0,0 +1,68 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Input, OnInit } from '@angular/core';
import { aggregationTranslations, AggregationType } from '@shared/models/time/time.models';
import { FormBuilder, FormGroup } from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component';
@Component({
selector: 'tb-aggregation-options-config-panel',
templateUrl: './aggregation-options-config-panel.component.html',
styleUrls: ['./aggregation-options-config-panel.component.scss']
})
export class AggregationOptionsConfigPanelComponent implements OnInit {
@Input()
allowedAggregationTypes: Array<AggregationType>;
@Input()
onClose: (result: Array<AggregationType> | null) => void;
@Input()
popoverComponent: TbPopoverComponent;
aggregationOptionsConfigForm: FormGroup;
aggregationTypes = AggregationType;
allAggregationTypes: Array<AggregationType> = Object.values(AggregationType);
aggregationTypesTranslations = aggregationTranslations;
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.aggregationOptionsConfigForm = this.fb.group({
allowedAggregationTypes: [this.allowedAggregationTypes?.length ? this.allowedAggregationTypes : this.allAggregationTypes]
});
}
update() {
if (this.onClose) {
const allowedAggregationTypes = this.aggregationOptionsConfigForm.get('allowedAggregationTypes').value;
// if full list selected returns empty for optimization
this.onClose(allowedAggregationTypes?.length < this.allAggregationTypes.length ? allowedAggregationTypes : []);
}
}
cancel() {
if (this.onClose) {
this.onClose(null);
}
}
}

30
ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.html

@ -0,0 +1,30 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field [formGroup]="aggregationTypeFormGroup"
[subscriptSizing]="subscriptSizing" [appearance]="appearance"
class="flex flex-1">
<mat-label *ngIf="displayLabel">{{ label }}</mat-label>
<mat-select [required]="required" formControlName="aggregationType">
<mat-option *ngFor="let type of aggregationTypes" [value]="type">
{{ displayAggregationTypeFn(type) }}
</mat-option>
</mat-select>
<ng-container matSuffix>
<ng-content select="[matSuffix]"></ng-content>
</ng-container>
</mat-form-field>

37
ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.scss

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.mat-mdc-form-field {
width: 100%;
}
}
:host ::ng-deep {
.mat-mdc-form-field-infix {
width: 100%;
}
.mat-mdc-select-value {
min-width: 100px;
}
.mat-mdc-form-field-has-icon-suffix {
.mat-mdc-text-field-wrapper:has(.mat-mdc-form-field-icon-suffix:empty) {
padding-right: 12px;
}
.mat-mdc-form-field-icon-suffix:empty {
padding: 0;
}
}
}

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

@ -0,0 +1,156 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { aggregationTranslations, AggregationType } from '@shared/models/time/time.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { isEqual } from '@core/utils';
@Component({
selector: 'tb-aggregation-type-select',
templateUrl: './aggregation-type-select.component.html',
styleUrls: ['./aggregation-type-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AggregationTypeSelectComponent),
multi: true
}]
})
export class AggregationTypeSelectComponent implements ControlValueAccessor, OnInit, OnChanges {
aggregationTypeFormGroup: FormGroup;
modelValue: AggregationType | null;
@Input()
allowedAggregationTypes: Array<AggregationType>;
@Input()
@coerceBoolean()
required: boolean;
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
displayLabel = true;
@Input()
labelText: string;
get label(): string {
if (this.labelText && this.labelText.length) {
return this.labelText;
}
return this.defaultLabel;
}
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
aggregationTypes: Array<AggregationType>;
private defaultLabel = this.translate.instant('aggregation.aggregation');
private allAggregationTypes: Array<AggregationType> = Object.values(AggregationType);
private propagateChange = (v: any) => { };
constructor(private translate: TranslateService,
private fb: FormBuilder) {
this.aggregationTypeFormGroup = this.fb.group({
aggregationType: [null]
});
this.aggregationTypeFormGroup.get('aggregationType').valueChanges.pipe(
takeUntilDestroyed()
).subscribe(
(value) => {
let modelValue;
if (!value) {
modelValue = null;
} else {
modelValue = value;
}
this.updateView(modelValue);
}
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.aggregationTypes = this.allowedAggregationTypes?.length ? this.allowedAggregationTypes : this.allAggregationTypes;
}
ngOnChanges({allowedAggregationTypes}: SimpleChanges): void {
if (allowedAggregationTypes && !allowedAggregationTypes.firstChange && !isEqual(allowedAggregationTypes.currentValue, allowedAggregationTypes.previousValue)) {
this.aggregationTypes = this.allowedAggregationTypes?.length ? this.allowedAggregationTypes : this.allAggregationTypes;
const currentAggregationType: AggregationType = this.aggregationTypeFormGroup.get('aggregationType').value;
if (currentAggregationType && !this.aggregationTypes.includes(currentAggregationType)) {
this.aggregationTypeFormGroup.get('aggregationType').patchValue(this.aggregationTypes[0], {emitEvent: true});
}
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.aggregationTypeFormGroup.disable();
} else {
this.aggregationTypeFormGroup.enable();
}
}
writeValue(value: AggregationType | null): void {
let aggregationType: AggregationType;
if (value && this.allowedAggregationTypes?.length && !this.allowedAggregationTypes.includes(value)) {
aggregationType = this.allowedAggregationTypes[0];
} else {
aggregationType = value;
}
this.modelValue = aggregationType;
this.aggregationTypeFormGroup.get('aggregationType').patchValue(aggregationType, {emitEvent: false});
}
updateView(value: AggregationType | null) {
if (this.modelValue !== value) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
displayAggregationTypeFn(aggregationType?: AggregationType | null): string | undefined {
if (aggregationType) {
return this.translate.instant(aggregationTranslations.get(aggregationType));
} else {
return '';
}
}
}

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

@ -0,0 +1,41 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section class="flex flex-1 flex-row gap-2" [formGroup]="timeintervalFormGroup">
<mat-form-field class="agg-interval flex flex-1" [class]="{'tb-inline-field': appearance === 'outline'}"
[subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-select formControlName="aggIntervals" multiple
placeholder="{{ 'timewindow.all' | translate }}">
<mat-select-trigger *ngIf="timeintervalFormGroup.get('aggIntervals').value?.length === allIntervals.length">
{{ 'timewindow.all' | translate }}
</mat-select-trigger>
<mat-option *ngFor="let interval of allIntervals" [value]="interval.value">
{{ interval.name | translate:interval.translateParams }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex flex-1" [class]="{'tb-inline-field': appearance === 'outline'}"
[subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-select formControlName="defaultAggInterval"
placeholder="{{ 'action.set' | translate }}">
<mat-option>--</mat-option>
<mat-option *ngFor="let interval of selectedIntervals" [value]="interval.value">
{{ interval.name | translate:interval.translateParams }}
</mat-option>
</mat-select>
</mat-form-field>
</section>

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

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../scss/constants';
:host {
.agg-interval {
max-width: 180px;
}
@media #{$mat-xs} {
min-width: 0;
width: 100%;
}
}

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

@ -0,0 +1,167 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TimeService } from '@core/services/time.service';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion';
import {
Interval,
intervalValuesToTimeIntervals,
TimeInterval,
TimewindowAggIntervalOptions
} from '@shared/models/time/time.models';
import { isDefined } from '@core/utils';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'tb-grouping-interval-options',
templateUrl: './grouping-interval-options.component.html',
styleUrls: ['./grouping-interval-options.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GroupingIntervalOptionsComponent),
multi: true
}
]
})
export class GroupingIntervalOptionsComponent implements OnInit, ControlValueAccessor {
@Input()
@coerceNumber()
min: number;
@Input()
@coerceNumber()
max: number;
@Input()
@coerceBoolean()
useCalendarIntervals = false;
@Input() disabled: boolean;
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
allIntervals: Array<TimeInterval>;
allIntervalValues: Array<Interval>;
selectedIntervals: Array<TimeInterval>;
timeintervalFormGroup: FormGroup;
private modelValue: TimewindowAggIntervalOptions;
private rendered = false;
private propagateChangeValue: any;
private propagateChange = (value: any) => {
this.propagateChangeValue = value;
};
constructor(private timeService: TimeService,
private fb: FormBuilder) {
this.timeintervalFormGroup = this.fb.group({
aggIntervals: [ [] ],
defaultAggInterval: [ null ],
});
this.timeintervalFormGroup.get('aggIntervals').valueChanges.pipe(
takeUntilDestroyed()
).subscribe(selectedIntervalValues => this.setSelectedIntervals(selectedIntervalValues));
this.timeintervalFormGroup.valueChanges.pipe(
takeUntilDestroyed()
).subscribe(() => this.updateView());
}
ngOnInit(): void {
this.updateIntervalsList();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
if (isDefined(this.propagateChangeValue)) {
this.propagateChange(this.propagateChangeValue);
}
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.timeintervalFormGroup.disable({emitEvent: false});
} else {
this.timeintervalFormGroup.enable({emitEvent: false});
}
}
writeValue(intervalOptions: TimewindowAggIntervalOptions): void {
this.modelValue = intervalOptions;
this.rendered = true;
this.timeintervalFormGroup.get('defaultAggInterval').patchValue(intervalOptions.defaultAggInterval, { emitEvent: false });
this.setIntervals(intervalOptions.aggIntervals);
}
private updateIntervalsList() {
this.allIntervals = this.timeService.getIntervals(this.min, this.max, this.useCalendarIntervals);
this.allIntervalValues = this.allIntervals.map(interval => interval.value);
}
private setIntervals(intervals: Array<Interval>) {
const selectedIntervals = !intervals?.length ? this.allIntervalValues : intervals;
this.timeintervalFormGroup.get('aggIntervals').patchValue(
selectedIntervals,
{emitEvent: false});
this.setSelectedIntervals(selectedIntervals);
}
private setSelectedIntervals(selectedIntervalValues: Array<Interval>) {
if (!selectedIntervalValues.length || selectedIntervalValues.length === this.allIntervalValues.length) {
this.selectedIntervals = this.allIntervals;
} else {
this.selectedIntervals = intervalValuesToTimeIntervals(selectedIntervalValues);
}
const defaultInterval: Interval = this.timeintervalFormGroup.get('defaultAggInterval').value;
if (defaultInterval && !selectedIntervalValues.includes(defaultInterval)) {
this.timeintervalFormGroup.get('defaultAggInterval').patchValue(null);
}
}
private updateView() {
if (!this.rendered) {
return;
}
let selectedIntervals: Array<Interval>;
const intervals: Array<Interval> = this.timeintervalFormGroup.get('aggIntervals').value;
if (intervals.length < this.allIntervals.length) {
selectedIntervals = intervals;
} else {
selectedIntervals = [];
}
this.modelValue = {
aggIntervals: selectedIntervals,
defaultAggInterval: this.timeintervalFormGroup.get('defaultAggInterval').value
};
this.propagateChange(this.modelValue);
}
}

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

@ -0,0 +1,85 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="intervalOptionsConfigForm" class="tb-interval-options-form tb-form-panel no-border"
[style]="aggregation ? {maxWidth: '600px'} : {maxWidth: '350px'}">
<div class="tb-form-panel-title">{{ 'timewindow.edit-intervals-list' | translate }}</div>
<div class="tb-form-hint tb-primary-fill">
{{ 'timewindow.edit-intervals-list-hint' | translate }} {{ aggregation ? ('timewindow.edit-grouping-intervals-list-hint' | translate) : '' }}
</div>
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell tb-interval" tbTruncateWithTooltip>
<mat-checkbox color="warn"
(click)="$event.stopPropagation()"
[indeterminate]="getIndeterminate()"
(change)="enableDisableIntervals($event.checked)"
[checked]="getChecked()">
</mat-checkbox>
{{ "timewindow.interval" | translate }}
</div>
<ng-container *ngIf="aggregation">
<div class="tb-form-table-header-cell tb-agg-interval-header" tbTruncateWithTooltip>{{"timewindow.allowed-agg-intervals" | translate }}</div>
<div class="tb-form-table-header-cell tb-agg-interval-header" tbTruncateWithTooltip>{{"timewindow.default-agg-interval" | translate }}</div>
</ng-container>
</div>
<div class="tb-form-table-body" formArrayName="intervals">
<ng-container *ngFor="let interval of intervalsFormArray.controls; let i = index; trackBy: trackByElement">
<div [formGroupName]="i" class="tb-form-table-row">
<div class="tb-form-table-row-cell tb-interval">
<mat-checkbox class="tb-interval-option" formControlName="enabled" color="primary">
<mat-label tbTruncateWithTooltip>{{ interval.get('name').value }}</mat-label>
</mat-checkbox>
</div>
<ng-container *ngIf="aggregation">
<div class="tb-form-table-row-cell tb-agg-interval">
<tb-grouping-interval-options
formControlName="aggIntervalsConfig"
[min]="minAggInterval(interval.get('value').value)"
[max]="maxAggInterval(interval.get('value').value)"
useCalendarIntervals
subscriptSizing="dynamic"
appearance="outline">
</tb-grouping-interval-options>
</div>
</ng-container>
</div>
</ng-container>
</div>
</div>
<div class="flex flex-row gap-2 items-center">
<button type="button"
mat-button
color="primary"
(click)="reset()">
{{ 'action.reset' | translate }}
</button>
<span class="flex-1"></span>
<button type="button"
mat-button
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="button"
mat-raised-button
color="primary"
(click)="update()"
[disabled]="intervalOptionsConfigForm.invalid || !intervalOptionsConfigForm.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</form>

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

@ -0,0 +1,71 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../scss/constants';
:host {
.tb-interval-options-form {
height: 100%;
.tb-form-table {
overflow: hidden;
}
.tb-form-table-body {
overflow-y: auto;
}
.tb-form-table-header-cell, .tb-form-table-row-cell {
&.tb-interval {
flex: 1 1 30%;
min-width: calc(30% - 8px);
@media #{$mat-gt-md} {
min-width: calc(30% - 12px);
}
}
&.tb-agg-interval {
flex: 1 1 70%;
min-width: 70%;
}
}
.tb-form-table-header-cell.tb-agg-interval-header {
flex: 1 1 35%;
width: 35%;
}
.tb-form-hint {
flex-shrink: 0;
}
::ng-deep {
.tb-interval-option {
display: block;
.mdc-form-field {
width: 100%;
}
.mdc-label {
overflow: hidden;
mat-label {
display: block;
}
}
}
}
}
}

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

@ -0,0 +1,231 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, DestroyRef, inject, Input, OnInit } from '@angular/core';
import {
HistoryWindowType,
QuickTimeInterval, quickTimeIntervalPeriod,
QuickTimeIntervalTranslationMap,
RealtimeWindowType,
TimewindowAggIntervalOptions,
TimewindowAggIntervalsConfig,
TimewindowInterval,
TimewindowIntervalOption,
TimewindowType
} from '@shared/models/time/time.models';
import { AbstractControl, FormBuilder, FormGroup, UntypedFormArray } from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { TimeService } from '@core/services/time.service';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TranslateService } from '@ngx-translate/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export interface IntervalOptionsConfigPanelData {
allowedIntervals: Array<TimewindowInterval>;
aggIntervalsConfig: TimewindowAggIntervalsConfig
}
@Component({
selector: 'tb-interval-options-config-panel',
templateUrl: './interval-options-config-panel.component.html',
styleUrls: ['./interval-options-config-panel.component.scss']
})
export class IntervalOptionsConfigPanelComponent implements OnInit {
@Input()
@coerceBoolean()
aggregation = false;
@Input()
allowedIntervals: Array<TimewindowInterval>;
@Input()
aggIntervalsConfig: TimewindowAggIntervalsConfig;
@Input()
intervalType: RealtimeWindowType | HistoryWindowType;
@Input()
timewindowType: TimewindowType;
@Input()
onClose: (result: IntervalOptionsConfigPanelData | null) => void;
@Input()
popoverComponent: TbPopoverComponent;
intervalOptionsConfigForm: FormGroup;
allIntervals: Array<TimewindowIntervalOption>;
allIntervalValues: Array<TimewindowInterval>
private timeIntervalTranslationMap = QuickTimeIntervalTranslationMap;
private destroyRef = inject(DestroyRef);
constructor(private fb: FormBuilder,
private timeService: TimeService,
private translate: TranslateService) {}
ngOnInit(): void {
if (this.intervalType === RealtimeWindowType.LAST_INTERVAL ||
this.intervalType === HistoryWindowType.LAST_INTERVAL) {
this.allIntervals = this.timeService.getIntervals(undefined, undefined, false);
this.allIntervalValues = this.allIntervals.map(interval => interval.value);
} else {
this.allIntervalValues = this.getQuickIntervals();
this.allIntervals = this.allIntervalValues.map(interval => ({
name: this.timeIntervalTranslationMap.get(interval as QuickTimeInterval),
value: interval
}));
}
this.intervalOptionsConfigForm = this.fb.group({
intervals: this.fb.array([])
});
const intervalControls: Array<AbstractControl> = [];
for (const interval of this.allIntervals) {
const intervalConfig: TimewindowAggIntervalOptions = this.aggIntervalsConfig?.hasOwnProperty(interval.value)
? this.aggIntervalsConfig[interval.value]
: null;
const intervalEnabled = this.allowedIntervals?.length ? this.allowedIntervals.includes(interval.value) : true;
const intervalControl = this.fb.group({
name: [this.translate.instant(interval.name, interval.translateParams)],
value: [interval.value],
enabled: [intervalEnabled],
aggIntervalsConfig: [{value: {
aggIntervals: intervalConfig?.aggIntervals ? intervalConfig.aggIntervals : [],
defaultAggInterval: intervalConfig?.defaultAggInterval ? intervalConfig.defaultAggInterval : null
}, disabled: !(intervalEnabled && this.aggregation)}]
});
if (this.aggregation) {
intervalControl.get('enabled').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((intervalEnabled) => {
if (intervalEnabled) {
intervalControl.get('aggIntervalsConfig').enable({emitEvent: false});
} else {
intervalControl.get('aggIntervalsConfig').disable({emitEvent: false});
}
});
}
intervalControls.push(intervalControl);
}
this.intervalOptionsConfigForm.setControl('intervals', this.fb.array(intervalControls), {emitEvent: false});
}
get intervalsFormArray(): UntypedFormArray {
return this.intervalOptionsConfigForm.get('intervals') as UntypedFormArray;
}
minAggInterval(interval: TimewindowInterval) {
return this.timeService.minIntervalLimit(this.getIntervalMs(interval));
}
maxAggInterval(interval: TimewindowInterval) {
return this.timeService.maxIntervalLimit(this.getIntervalMs(interval));
}
private getIntervalMs(interval: TimewindowInterval): number {
if (this.intervalType === RealtimeWindowType.INTERVAL ||
this.intervalType === HistoryWindowType.INTERVAL) {
return quickTimeIntervalPeriod(interval as QuickTimeInterval);
}
return interval as number;
}
trackByElement(i: number, item: any) {
return item;
}
update() {
if (this.onClose) {
const allowedIntervals: Array<TimewindowInterval> = [];
const aggIntervalsConfig: TimewindowAggIntervalsConfig = {};
const intervalOptionsConfig = this.intervalOptionsConfigForm.get('intervals').value;
for (const interval of intervalOptionsConfig) {
if (interval.enabled) {
allowedIntervals.push(interval.value);
if (this.aggregation && (interval.aggIntervalsConfig.aggIntervals.length || interval.aggIntervalsConfig.defaultAggInterval)) {
const intervalParams: TimewindowAggIntervalOptions = {};
if (interval.aggIntervalsConfig.aggIntervals.length) {
intervalParams.aggIntervals = interval.aggIntervalsConfig.aggIntervals;
}
if (interval.aggIntervalsConfig.defaultAggInterval) {
intervalParams.defaultAggInterval = interval.aggIntervalsConfig.defaultAggInterval;
}
aggIntervalsConfig[interval.value] = intervalParams;
}
}
}
this.onClose({
// if full list selected returns empty for optimization
allowedIntervals: allowedIntervals?.length < this.allIntervals.length ? allowedIntervals : [],
aggIntervalsConfig
});
}
}
cancel() {
if (this.onClose) {
this.onClose(null);
}
}
reset() {
const intervalControls = this.intervalsFormArray.controls;
for (const interval of intervalControls) {
interval.patchValue({
enabled: true,
aggIntervalsConfig: {
aggIntervals: [],
defaultAggInterval: null
}
});
}
this.intervalOptionsConfigForm.markAsDirty();
}
private getQuickIntervals(): Array<QuickTimeInterval> {
const allQuickIntervals = Object.values(QuickTimeInterval);
if (this.timewindowType === TimewindowType.REALTIME) {
return allQuickIntervals.filter(interval => interval.startsWith('CURRENT_'));
}
return allQuickIntervals;
}
getIndeterminate(): boolean {
const enabledIntervals = this.intervalsFormArray.value.filter(interval => interval.enabled);
return enabledIntervals.length !== 0 && enabledIntervals.length !== this.allIntervals.length;
}
enableDisableIntervals(allEnabled: boolean) {
const intervalControls = this.intervalsFormArray.controls;
for (const interval of intervalControls) {
interval.patchValue({
enabled: allEnabled
});
}
this.intervalOptionsConfigForm.markAsDirty();
}
getChecked(): boolean {
const intervals = this.intervalsFormArray.value;
return intervals.every(interval => interval.enabled);
}
}

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

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

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

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

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

@ -14,11 +14,13 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/models/time/time.models'; import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/models/time/time.models';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { isEqual } from '@core/utils';
@Component({ @Component({
selector: 'tb-quick-time-interval', 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; modelValue: QuickTimeInterval;
timeIntervalTranslationMap = QuickTimeIntervalTranslationMap; timeIntervalTranslationMap = QuickTimeIntervalTranslationMap;
@ -49,25 +51,53 @@ export class QuickTimeIntervalComponent implements OnInit, ControlValueAccessor
@Input() onlyCurrentInterval = false; @Input() onlyCurrentInterval = false;
@Input()
allowedIntervals: Array<QuickTimeInterval>
@Input() @Input()
subscriptSizing: SubscriptSizing = 'fixed'; subscriptSizing: SubscriptSizing = 'fixed';
@Input() @Input()
appearance: MatFormFieldAppearance = 'fill'; appearance: MatFormFieldAppearance = 'fill';
intervals: Array<QuickTimeInterval>;
private allAvailableIntervals: Array<QuickTimeInterval>;
quickIntervalFormGroup: FormGroup;
private propagateChange = (_: any) => {}; 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() { ngOnInit(): void {
if (this.onlyCurrentInterval) { this.allAvailableIntervals = this.getAllAvailableIntervals();
return this.allIntervals.filter(interval => interval.startsWith('CURRENT_')); this.intervals = this.allowedIntervals?.length ? this.allowedIntervals : this.allAvailableIntervals;
}
return this.allIntervals;
} }
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 { registerOnChange(fn: any): void {
@ -79,13 +109,35 @@ export class QuickTimeIntervalComponent implements OnInit, ControlValueAccessor
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; 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.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() { private getAllAvailableIntervals() {
this.propagateChange(this.modelValue); if (this.onlyCurrentInterval) {
return this.allIntervals.filter(interval => interval.startsWith('CURRENT_'));
}
return this.allIntervals;
} }
} }

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

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

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

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

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

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

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

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

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

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

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

@ -20,17 +20,14 @@
.tb-timewindow-form { .tb-timewindow-form {
overflow: hidden; overflow: hidden;
.tb-flex {
gap: 16px;
}
&-content { &-content {
overflow-y: auto; overflow-y: auto;
tb-timeinterval, tb-timeinterval,
tb-quick-time-interval, tb-quick-time-interval,
tb-datetime-period, tb-datetime-period,
tb-datapoints-limit { tb-datapoints-limit,
tb-aggregation-type-select {
flex: 1; flex: 1;
} }
} }

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

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

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

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

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

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

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

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

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

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

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

@ -80,7 +80,8 @@
"clear": "Clear", "clear": "Clear",
"upload": "Upload", "upload": "Upload",
"delete-anyway": "Delete anyway", "delete-anyway": "Delete anyway",
"delete-selected": "Delete selected" "delete-selected": "Delete selected",
"set": "Set"
}, },
"aggregation": { "aggregation": {
"aggregation": "Aggregation", "aggregation": "Aggregation",
@ -4778,7 +4779,16 @@
"hide-group-interval": "Hide grouping interval from end-users", "hide-group-interval": "Hide grouping interval from end-users",
"hide-max-values": "Hide max values from end-users", "hide-max-values": "Hide max values from end-users",
"hide-timezone": "Hide time zone 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": { "tooltip": {
"trigger": "Trigger", "trigger": "Trigger",

Loading…
Cancel
Save