From 034af204f02aedbf5440ed4fe34b2ccddc88c746 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 8 Dec 2020 18:01:17 +0200 Subject: [PATCH] UI: Added key name autocomplete in alarms rules to device profile --- .../app/core/http/device-profile.service.ts | 16 +++ .../filter/key-filter-dialog.component.html | 18 ++- .../filter/key-filter-dialog.component.ts | 124 ++++++++++++++---- .../filter/key-filter-list.component.ts | 13 +- .../add-device-profile-dialog.component.html | 3 +- ...alarm-rule-condition-dialog.component.html | 1 + .../alarm-rule-condition-dialog.component.ts | 7 +- .../alarm/alarm-rule-condition.component.ts | 7 +- .../profile/alarm/alarm-rule.component.html | 2 +- .../profile/alarm/alarm-rule.component.ts | 4 + .../alarm/create-alarm-rules.component.html | 2 +- .../alarm/create-alarm-rules.component.ts | 6 +- .../alarm/device-profile-alarm.component.html | 6 +- .../alarm/device-profile-alarm.component.ts | 4 + .../device-profile-alarms.component.html | 1 + .../alarm/device-profile-alarms.component.ts | 4 + .../profile/device-profile.component.html | 3 +- .../profile/device-profile.component.ts | 5 + .../device-wizard-dialog.component.html | 5 +- .../device-profile-tabs.component.html | 2 +- 20 files changed, 188 insertions(+), 45 deletions(-) diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts index c535878fd2..590bdaa20e 100644 --- a/ui-ngx/src/app/core/http/device-profile.service.ts +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -69,4 +69,20 @@ export class DeviceProfileService { return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } + public getDeviceProfileDevicesAttributesKeys(deviceProfileId?: string, config?: RequestConfig): Observable> { + let url = `/api/deviceProfile/devices/keys/attributes`; + if (isDefinedAndNotNull(deviceProfileId)) { + url += `?deviceProfileId=${deviceProfileId}`; + } + return this.http.get>(url, defaultHttpOptionsFromConfig(config)); + } + + public getDeviceProfileDevicesTimeseriesKeys(deviceProfileId?: string, config?: RequestConfig): Observable> { + let url = `/api/deviceProfile/devices/keys/timeseries`; + if (isDefinedAndNotNull(deviceProfileId)) { + url += `?deviceProfileId=${deviceProfileId}`; + } + return this.http.get>(url, defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html index 5a9f8858c3..0fe8ed1777 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html @@ -40,11 +40,19 @@ filter.key-name - - - {{option}} + #keyNameInput + (focusin)="onFocus()" + [matAutocomplete]="keyName" + [matAutocompleteDisabled]="!showAutocomplete"> + + + + diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts index 202d37be34..b683c5cb17 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { Component, ElementRef, Inject, OnDestroy, OnInit, SkipSelf, ViewChild } from '@angular/core'; import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; @@ -32,9 +32,12 @@ import { } from '@shared/models/query/query.models'; import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; -import { EntityField, entityFields } from '@shared/models/entity.models'; -import { Observable } from 'rxjs'; -import { filter, map, startWith } from 'rxjs/operators'; +import { entityFields } from '@shared/models/entity.models'; +import { Observable, of, Subject } from 'rxjs'; +import { filter, map, mergeMap, publishReplay, refCount, startWith, takeUntil } from 'rxjs/operators'; +import { isDefined } from '@core/utils'; +import { EntityId } from '@shared/models/id/entity-id'; +import { DeviceProfileService } from '@core/http/device-profile.service'; export interface KeyFilterDialogData { keyFilter: KeyFilterInfo; @@ -43,6 +46,7 @@ export interface KeyFilterDialogData { allowUserDynamicSource: boolean; readonly: boolean; telemetryKeysOnly: boolean; + entityId?: EntityId; } @Component({ @@ -53,7 +57,13 @@ export interface KeyFilterDialogData { }) export class KeyFilterDialogComponent extends DialogComponent - implements OnInit, ErrorStateMatcher { + implements OnInit, OnDestroy, ErrorStateMatcher { + + @ViewChild('keyNameInput', {static: true}) private keyNameInput: ElementRef; + + private dirty = false; + private entityKeysName: Observable>; + private destroy$ = new Subject(); keyFilterFormGroup: FormGroup; @@ -72,19 +82,18 @@ export class KeyFilterDialogComponent extends submitted = false; - entityFields: { [fieldName: string]: EntityField }; - - entityFieldsList: string[]; + showAutocomplete = false; - readonly entityField = EntityKeyType.ENTITY_FIELD; + filteredKeysName: Observable>; - filteredEntityFields: Observable; + searchText = ''; constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: KeyFilterDialogData, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, public dialogRef: MatDialogRef, + private deviceProfileService: DeviceProfileService, private dialogs: DialogService, private translate: TranslateService, private fb: FormBuilder) { @@ -104,7 +113,9 @@ export class KeyFilterDialogComponent extends ); if (!this.data.readonly) { - this.keyFilterFormGroup.get('valueType').valueChanges.subscribe((valueType: EntityKeyValueType) => { + this.keyFilterFormGroup.get('valueType').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((valueType: EntityKeyValueType) => { const prevValue: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value; if (prevValue && prevValue !== valueType && predicates && predicates.length) { @@ -121,11 +132,26 @@ export class KeyFilterDialogComponent extends } }); + this.keyFilterFormGroup.get('key.type').valueChanges.pipe( + startWith(this.data.keyFilter.key.type), + takeUntil(this.destroy$) + ).subscribe((type: EntityKeyType) => { + if (type === EntityKeyType.ENTITY_FIELD || isDefined(this.data.entityId)) { + this.entityKeysName = null; + this.dirty = false; + this.showAutocomplete = true; + } else { + this.showAutocomplete = false; + } + }); + this.keyFilterFormGroup.get('key.key').valueChanges.pipe( - filter((keyName) => this.keyFilterFormGroup.get('key.type').value === this.entityField && this.entityFields.hasOwnProperty(keyName)) + filter((keyName) => + this.keyFilterFormGroup.get('key.type').value === EntityKeyType.ENTITY_FIELD && entityFields.hasOwnProperty(keyName)), + takeUntil(this.destroy$) ).subscribe((keyName: string) => { const prevValueType: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; - const newValueType = this.entityFields[keyName]?.time ? EntityKeyValueType.DATE_TIME : EntityKeyValueType.STRING; + const newValueType = entityFields[keyName]?.time ? EntityKeyValueType.DATE_TIME : EntityKeyValueType.STRING; if (prevValueType !== newValueType) { this.keyFilterFormGroup.get('valueType').patchValue(newValueType, {emitEvent: false}); } @@ -133,18 +159,20 @@ export class KeyFilterDialogComponent extends } else { this.keyFilterFormGroup.disable({emitEvent: false}); } + } - this.entityFields = entityFields; - this.entityFieldsList = Object.values(entityFields).map(entityField => entityField.keyName).sort(); + ngOnInit() { + this.filteredKeysName = this.keyFilterFormGroup.get('key.key').valueChanges + .pipe( + map(value => value ? value : ''), + mergeMap(name => this.fetchEntityName(name)) + ); } - ngOnInit(): void { - this.filteredEntityFields = this.keyFilterFormGroup.get('key.key').valueChanges.pipe( - startWith(''), - map(value => { - return this.entityFieldsList.filter(option => option.startsWith(value)); - }) - ); + ngOnDestroy() { + super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); } isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { @@ -157,6 +185,21 @@ export class KeyFilterDialogComponent extends this.dialogRef.close(null); } + clear() { + this.keyFilterFormGroup.get('key.key').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.keyNameInput.nativeElement.blur(); + this.keyNameInput.nativeElement.focus(); + }, 0); + } + + onFocus() { + if (!this.dirty && this.showAutocomplete) { + this.keyFilterFormGroup.get('key.key').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = true; + } + } + save(): void { this.submitted = true; if (this.keyFilterFormGroup.valid) { @@ -164,4 +207,41 @@ export class KeyFilterDialogComponent extends this.dialogRef.close(keyFilter); } } + + private fetchEntityName(searchText?: string): Observable> { + this.searchText = searchText; + return this.getEntityKeys().pipe( + map(keys => searchText ? keys.filter(key => key.toUpperCase().startsWith(searchText.toUpperCase())) : keys) + ); + } + + private getEntityKeys(): Observable> { + if (!this.entityKeysName) { + let keyNameObservable: Observable>; + switch (this.keyFilterFormGroup.get('key.type').value) { + case EntityKeyType.ENTITY_FIELD: + keyNameObservable = of(Object.values(entityFields).map(entityField => entityField.keyName).sort()); + break; + case EntityKeyType.ATTRIBUTE: + keyNameObservable = this.deviceProfileService.getDeviceProfileDevicesAttributesKeys( + this.data.entityId?.id, + {ignoreLoading: true} + ); + break; + case EntityKeyType.TIME_SERIES: + keyNameObservable = this.deviceProfileService.getDeviceProfileDevicesTimeseriesKeys( + this.data.entityId?.id, + {ignoreLoading: true} + ); + break; + default: + keyNameObservable = of([]); + } + this.entityKeysName = keyNameObservable.pipe( + publishReplay(1), + refCount() + ); + } + return this.entityKeysName; + } } diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts index 2779a35be9..11cb29f149 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts @@ -19,7 +19,8 @@ import { AbstractControl, ControlValueAccessor, FormArray, - FormBuilder, FormControl, + FormBuilder, + FormControl, FormGroup, NG_VALUE_ACCESSOR, Validators @@ -28,12 +29,13 @@ import { Observable, Subscription } from 'rxjs'; import { EntityKeyType, entityKeyTypeTranslationMap, - KeyFilter, - KeyFilterInfo, keyFilterInfosToKeyFilters + KeyFilterInfo, + keyFilterInfosToKeyFilters } from '@shared/models/query/query.models'; import { MatDialog } from '@angular/material/dialog'; import { deepClone } from '@core/utils'; import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component'; +import { EntityId } from '@shared/models/id/entity-id'; @Component({ selector: 'tb-key-filter-list', @@ -57,6 +59,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { @Input() telemetryKeysOnly = false; + @Input() entityId: EntityId; + keyFilterListFormGroup: FormGroup; entityKeyTypeTranslations = entityKeyTypeTranslationMap; @@ -170,7 +174,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { readonly: this.disabled, displayUserParameters: this.displayUserParameters, allowUserDynamicSource: this.allowUserDynamicSource, - telemetryKeysOnly: this.telemetryKeysOnly + telemetryKeysOnly: this.telemetryKeysOnly, + entityId: this.entityId } }).afterClosed(); } diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html index c696272d78..2fa920f541 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -96,7 +96,8 @@ {count: alarmRulesFormGroup.get('alarms').value ? alarmRulesFormGroup.get('alarms').value.length : 0} }} + formControlName="alarms" + [deviceProfileId]="null"> diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html index 493763756b..b7d13cdbeb 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html @@ -34,6 +34,7 @@ [displayUserParameters]="false" [allowUserDynamicSource]="false" [telemetryKeysOnly]="true" + [entityId]="entityId" formControlName="keyFilters">
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts index 4055560b38..0c81ebf397 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts @@ -22,15 +22,16 @@ import { AppState } from '@core/core.state'; import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; -import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; -import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models'; +import { keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models'; import { AlarmCondition, AlarmConditionType, AlarmConditionTypeTranslationMap } from '@shared/models/device.models'; import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; +import { EntityId } from '@shared/models/id/entity-id'; export interface AlarmRuleConditionDialogData { readonly: boolean; condition: AlarmCondition; + entityId?: EntityId; } @Component({ @@ -50,6 +51,7 @@ export class AlarmRuleConditionDialogComponent extends DialogComponent, private fb: FormBuilder, - private utils: UtilsService, public translate: TranslateService) { super(store, router, dialogRef); diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts index 23e52cf8cd..301175c485 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts @@ -33,6 +33,7 @@ import { AlarmRuleConditionDialogData } from '@home/components/profile/alarm/alarm-rule-condition-dialog.component'; import { TimeUnit } from '@shared/models/time/time.models'; +import { EntityId } from '@shared/models/id/entity-id'; @Component({ selector: 'tb-alarm-rule-condition', @@ -56,6 +57,9 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit @Input() disabled: boolean; + @Input() + deviceProfileId: EntityId; + alarmRuleConditionFormGroup: FormGroup; specText = ''; @@ -123,7 +127,8 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { readonly: this.disabled, - condition: this.disabled ? this.modelValue : deepClone(this.modelValue) + condition: this.disabled ? this.modelValue : deepClone(this.modelValue), + entityId: this.deviceProfileId } }).afterClosed().subscribe((result) => { if (result) { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html index c7c77a7f3c..75772663c3 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html @@ -16,7 +16,7 @@ -->
- + diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts index 1e7806b455..831f92d0d4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts @@ -33,6 +33,7 @@ import { EditAlarmDetailsDialogComponent, EditAlarmDetailsDialogData } from '@home/components/profile/alarm/edit-alarm-details-dialog.component'; +import { EntityId } from '@shared/models/id/entity-id'; @Component({ selector: 'tb-alarm-rule', @@ -65,6 +66,9 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat this.requiredValue = coerceBooleanProperty(value); } + @Input() + deviceProfileId: EntityId; + private modelValue: AlarmRule; alarmRuleFormGroup: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html index 5fa0733905..25820aa07e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html @@ -35,7 +35,7 @@ - +