diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index cd6d8bf787..e0fe50fd3d 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -576,7 +576,7 @@ export class EntityService { } private getEntityFieldKeys (entityType: EntityType, searchText: string): Array { - const entityFieldKeys: string[] = []; + const entityFieldKeys: string[] = [entityFields.createdTime.keyName]; const query = searchText.toLowerCase(); switch(entityType) { case EntityType.USER: diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html index 3309a17c94..22dafcec14 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html @@ -16,7 +16,7 @@ -->
- + @@ -24,7 +24,7 @@ - + {{ (booleanFilterPredicateFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts index 2b54fa0fca..237154814b 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts @@ -39,8 +39,6 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On @Input() disabled: boolean; - @Input() userMode: boolean; - booleanFilterPredicateFormGroup: FormGroup; booleanOperations = Object.keys(BooleanOperation); @@ -57,9 +55,6 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On operation: [BooleanOperation.EQUAL, [Validators.required]], value: [false] }); - if (this.userMode) { - this.booleanFilterPredicateFormGroup.get('operation').disable({emitEvent: false}); - } this.booleanFilterPredicateFormGroup.valueChanges.subscribe(() => { this.updateModel(); }); diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html index d2a874fae9..32fc06a2e1 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html @@ -36,9 +36,9 @@ diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts index 7922512c65..09607d1690 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts @@ -24,14 +24,14 @@ import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { BooleanOperation, booleanOperationTranslationMap, - ComplexFilterPredicate, ComplexOperation, complexOperationTranslationMap, + ComplexFilterPredicate, ComplexFilterPredicateInfo, ComplexOperation, complexOperationTranslationMap, EntityKeyValueType, - FilterPredicateType + FilterPredicateType, KeyFilterPredicateInfo } from '@shared/models/query/query.models'; export interface ComplexFilterPredicateDialogData { - complexPredicate: ComplexFilterPredicate; - userMode: boolean; + complexPredicate: ComplexFilterPredicateInfo; + key: string; disabled: boolean; isAdd: boolean; valueType: EntityKeyValueType; @@ -44,7 +44,7 @@ export interface ComplexFilterPredicateDialogData { styleUrls: [] }) export class ComplexFilterPredicateDialogComponent extends - DialogComponent + DialogComponent implements OnInit, ErrorStateMatcher { complexFilterFormGroup: FormGroup; @@ -61,7 +61,7 @@ export class ComplexFilterPredicateDialogComponent extends protected router: Router, @Inject(MAT_DIALOG_DATA) public data: ComplexFilterPredicateDialogData, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, - public dialogRef: MatDialogRef, + public dialogRef: MatDialogRef, private fb: FormBuilder) { super(store, router, dialogRef); @@ -91,7 +91,7 @@ export class ComplexFilterPredicateDialogComponent extends save(): void { this.submitted = true; if (this.complexFilterFormGroup.valid) { - const predicate: ComplexFilterPredicate = this.complexFilterFormGroup.getRawValue(); + const predicate: ComplexFilterPredicateInfo = this.complexFilterFormGroup.getRawValue(); predicate.type = FilterPredicateType.COMPLEX; this.dialogRef.close(predicate); } diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts index 5ec3b712f6..8a9b5a8531 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts @@ -16,7 +16,11 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ComplexFilterPredicate, EntityKeyValueType } from '@shared/models/query/query.models'; +import { + ComplexFilterPredicate, + ComplexFilterPredicateInfo, + EntityKeyValueType +} from '@shared/models/query/query.models'; import { MatDialog } from '@angular/material/dialog'; import { ComplexFilterPredicateDialogComponent, @@ -40,13 +44,13 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On @Input() disabled: boolean; - @Input() userMode: boolean; - @Input() valueType: EntityKeyValueType; + @Input() key: string; + private propagateChange = null; - private complexFilterPredicate: ComplexFilterPredicate; + private complexFilterPredicate: ComplexFilterPredicateInfo; constructor(private dialog: MatDialog) { } @@ -65,21 +69,21 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On this.disabled = isDisabled; } - writeValue(predicate: ComplexFilterPredicate): void { + writeValue(predicate: ComplexFilterPredicateInfo): void { this.complexFilterPredicate = predicate; } private openComplexFilterDialog() { this.dialog.open(ComplexFilterPredicateDialogComponent, { + ComplexFilterPredicateInfo>(ComplexFilterPredicateDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { complexPredicate: deepClone(this.complexFilterPredicate), disabled: this.disabled, - userMode: this.userMode, valueType: this.valueType, - isAdd: false + isAdd: false, + key: this.key } }).afterClosed().subscribe( (result) => { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html index 99c5848237..ab49dae445 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html @@ -17,7 +17,7 @@ -->
-

{{ userMode ? filter.filter : ((isAdd ? 'filter.add' : 'filter.edit') | translate) }}

+

{{ (isAdd ? 'filter.add' : 'filter.edit') | translate }}

+
+
+
+ + {{ 'filter.editable' | translate }} + +
+ + filter.display-label + + + + {{ 'filter.autogenerated-label' | translate }} + +
+ + filter.order-priority + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts new file mode 100644 index 0000000000..fe4063bb12 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts @@ -0,0 +1,108 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +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 { + BooleanOperation, + EntityKeyValueType, generateUserFilterValueLabel, + KeyFilterPredicateUserInfo, NumericOperation, + StringOperation +} from '@shared/models/query/query.models'; +import { TranslateService } from '@ngx-translate/core'; + +export interface FilterUserInfoDialogData { + key: string; + valueType: EntityKeyValueType; + operation: StringOperation | BooleanOperation | NumericOperation; + keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo; +} + +@Component({ + selector: 'tb-filter-user-info-dialog', + templateUrl: './filter-user-info-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: FilterUserInfoDialogComponent}], + styleUrls: [] +}) +export class FilterUserInfoDialogComponent extends + DialogComponent + implements OnInit, ErrorStateMatcher { + + filterUserInfoFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: FilterUserInfoDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private translate: TranslateService) { + super(store, router, dialogRef); + + this.filterUserInfoFormGroup = this.fb.group( + { + editable: [this.data.keyFilterPredicateUserInfo.editable], + label: [this.data.keyFilterPredicateUserInfo.label], + autogeneratedLabel: [this.data.keyFilterPredicateUserInfo.autogeneratedLabel], + order: [this.data.keyFilterPredicateUserInfo.order] + } + ); + this.onAutogeneratedLabelChange(); + this.filterUserInfoFormGroup.get('autogeneratedLabel').valueChanges.subscribe(() => { + this.onAutogeneratedLabelChange(); + }); + } + + private onAutogeneratedLabelChange() { + const autogeneratedLabel: boolean = this.filterUserInfoFormGroup.get('autogeneratedLabel').value; + if (autogeneratedLabel) { + const generatedLabel = generateUserFilterValueLabel(this.data.key, this.data.valueType, this.data.operation, this.translate); + this.filterUserInfoFormGroup.get('label').patchValue(generatedLabel, {emitEvent: false}); + this.filterUserInfoFormGroup.get('label').disable({emitEvent: false}); + } else { + this.filterUserInfoFormGroup.get('label').enable({emitEvent: false}); + } + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + if (this.filterUserInfoFormGroup.valid) { + const keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo = this.filterUserInfoFormGroup.getRawValue(); + this.dialogRef.close(keyFilterPredicateUserInfo); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.html new file mode 100644 index 0000000000..bcdae3452f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.html @@ -0,0 +1,26 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts new file mode 100644 index 0000000000..968e6c95e9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts @@ -0,0 +1,104 @@ +/// +/// Copyright © 2016-2020 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, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { + BooleanOperation, + EntityKeyValueType, + KeyFilterPredicateUserInfo, NumericOperation, + StringOperation +} from '@shared/models/query/query.models'; +import { MatDialog } from '@angular/material/dialog'; +import { + FilterUserInfoDialogComponent, + FilterUserInfoDialogData +} from '@home/components/filter/filter-user-info-dialog.component'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-filter-user-info', + templateUrl: './filter-user-info.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterUserInfoComponent), + multi: true + } + ] +}) +export class FilterUserInfoComponent implements ControlValueAccessor, OnInit { + + @Input() disabled: boolean; + + @Input() key: string; + + @Input() operation: StringOperation | BooleanOperation | NumericOperation; + + @Input() valueType: EntityKeyValueType; + + private propagateChange = null; + + private keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo; + + constructor(private dialog: MatDialog) { + } + + ngOnInit(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo): void { + this.keyFilterPredicateUserInfo = keyFilterPredicateUserInfo; + } + + private openFilterUserInfoDialog() { + this.dialog.open(FilterUserInfoDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + keyFilterPredicateUserInfo: deepClone(this.keyFilterPredicateUserInfo), + valueType: this.valueType, + key: this.key, + operation: this.operation + } + }).afterClosed().subscribe( + (result) => { + if (result) { + this.keyFilterPredicateUserInfo = result; + this.updateModel(); + } + } + ); + } + + private updateModel() { + this.propagateChange(this.keyFilterPredicateUserInfo); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts index eddfc562cc..cf48098a3f 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts @@ -184,8 +184,7 @@ export class FiltersDialogComponent extends DialogComponent { if (result) { diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts index 1992b5b90c..3477f32a99 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts @@ -18,8 +18,8 @@ import { Component, Inject, InjectionToken } from '@angular/core'; import { IAliasController } from '@core/api/widget-api.models'; import { Filter, FilterInfo } from '@shared/models/query/query.models'; import { MatDialog } from '@angular/material/dialog'; -import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; import { deepClone } from '@core/utils'; +import { UserFilterDialogComponent, UserFilterDialogData } from '@home/components/filter/user-filter-dialog.component'; export const FILTER_EDIT_PANEL_DATA = new InjectionToken('FiltersEditPanelData'); @@ -44,15 +44,12 @@ export class FiltersEditPanelComponent { public editFilter(filterId: string, filter: FilterInfo) { const singleFilter: Filter = {id: filterId, ...deepClone(filter)}; - this.dialog.open(FilterDialogComponent, { + this.dialog.open(UserFilterDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - isAdd: false, - filters: [], - filter: singleFilter, - userMode: true + filter: singleFilter } }).afterClosed().subscribe( (result) => { diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts index 0beb39679f..4019f0d24d 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts @@ -22,7 +22,7 @@ import { TranslateService } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; import { BreakpointObserver } from '@angular/cdk/layout'; import { deepClone } from '@core/utils'; -import { FilterInfo } from '@shared/models/query/query.models'; +import { FilterInfo, isFilterEditable } from '@shared/models/query/query.models'; import { FILTER_EDIT_PANEL_DATA, FiltersEditPanelComponent, @@ -144,7 +144,7 @@ export class FiltersEditComponent implements OnInit, OnDestroy { this.hasEditableFilters = false; for (const filterId of Object.keys(allFilters)) { const filterInfo = this.aliasController.getFilterInfo(filterId); - if (filterInfo && filterInfo.editable) { + if (filterInfo && isFilterEditable(filterInfo)) { this.filtersInfo[filterId] = deepClone(filterInfo); this.hasEditableFilters = true; } 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 332f2a1274..4a555eff2f 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 @@ -17,7 +17,7 @@ -->
-

{{data.userMode ? data.keyFilter.key.key : ((data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter') | translate)}}

+

{{(data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter') | translate}}

+
+ + +
+
+
+
+
+ + + {{ userInputControl.get('label').value }} + + + + + + {{ userInputControl.get('label').value }} + + + + + + + + + + {{ userInputControl.get('label').value }} + + +
+
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts new file mode 100644 index 0000000000..8d1a6e95a9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts @@ -0,0 +1,118 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + AbstractControl, FormArray, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgForm, + Validators +} from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { TranslateService } from '@ngx-translate/core'; +import { + EntityKeyValueType, + Filter, + filterToUserFilterInfoList, + UserFilterInputInfo +} from '@shared/models/query/query.models'; + +export interface UserFilterDialogData { + filter: Filter; +} + +@Component({ + selector: 'tb-user-filter-dialog', + templateUrl: './user-filter-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: UserFilterDialogComponent}], + styleUrls: [] +}) +export class UserFilterDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + filter: Filter; + + userFilterFormGroup: FormGroup; + + valueTypeEnum = EntityKeyValueType; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: UserFilterDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + public translate: TranslateService) { + super(store, router, dialogRef); + this.filter = data.filter; + const userInputs = filterToUserFilterInfoList(this.filter, translate); + + const userInputControls: Array = []; + for (const userInput of userInputs) { + userInputControls.push(this.createUserInputFormControl(userInput)); + } + + this.userFilterFormGroup = this.fb.group({ + userInputs: this.fb.array(userInputControls) + }); + } + + private createUserInputFormControl(userInput: UserFilterInputInfo): AbstractControl { + const userInputControl = this.fb.group({ + label: [userInput.label], + valueType: [userInput.valueType], + value: [(userInput.info.keyFilterPredicate as any).value, + userInput.valueType === EntityKeyValueType.NUMERIC || + userInput.valueType === EntityKeyValueType.DATE_TIME ? [Validators.required] : []] + }); + userInputControl.get('value').valueChanges.subscribe(value => { + (userInput.info.keyFilterPredicate as any).value = value; + }); + return userInputControl; + } + + userInputsFormArray(): FormArray { + return this.userFilterFormGroup.get('userInputs') as FormArray; + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.dialogRef.close(this.filter); + } +} diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index b4c50462bc..af508dbb6c 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -80,6 +80,9 @@ import { FilterDialogComponent } from '@home/components/filter/filter-dialog.com import { FilterSelectComponent } from './filter/filter-select.component'; import { FiltersEditComponent } from '@home/components/filter/filters-edit.component'; import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.component'; +import { UserFilterDialogComponent } from '@home/components/filter/user-filter-dialog.component'; +import { FilterUserInfoComponent } from './filter/filter-user-info.component'; +import { FilterUserInfoDialogComponent } from './filter/filter-user-info-dialog.component'; @NgModule({ declarations: @@ -142,7 +145,10 @@ import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit- FiltersDialogComponent, FilterSelectComponent, FiltersEditComponent, - FiltersEditPanelComponent + FiltersEditPanelComponent, + UserFilterDialogComponent, + FilterUserInfoComponent, + FilterUserInfoDialogComponent ], imports: [ CommonModule, @@ -197,7 +203,8 @@ import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit- FilterDialogComponent, FiltersDialogComponent, FilterSelectComponent, - FiltersEditComponent + FiltersEditComponent, + UserFilterDialogComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index fd34296423..bec7b7172b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -633,7 +633,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } else { let label: string = chip; if (type === DataKeyType.alarm || type === DataKeyType.entityField) { - const keyField = type === DataKeyType.alarm ? alarmFields[label] : entityFields[chip];; + const keyField = type === DataKeyType.alarm ? alarmFields[label] : entityFields[chip]; if (keyField) { label = this.translate.instant(keyField.name); } @@ -729,8 +729,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont data: { isAdd: true, filters: this.filters, - filter: singleFilter, - userMode: false + filter: singleFilter } }).afterClosed().pipe( tap((result) => { diff --git a/ui-ngx/src/app/shared/components/time/datetime.component.html b/ui-ngx/src/app/shared/components/time/datetime.component.html index 51a8b193c0..d2262a7e91 100644 --- a/ui-ngx/src/app/shared/components/time/datetime.component.html +++ b/ui-ngx/src/app/shared/components/time/datetime.component.html @@ -16,8 +16,8 @@ -->
- - {{ dateText | translate }} + + {{ dateText | translate }} - - {{ timeText | translate }} + + {{ timeText | translate }} , @Optional() @@ -305,9 +313,51 @@ export class TbSnackBarComponent implements AfterViewInit, OnDestroy { } ngAfterViewInit() { + if (this.snackBarRef) { + this.parentEl = this.data.parent.nativeElement; + this.snackBarContainerEl = this.elementRef.nativeElement.parentNode; + this.snackBarContainerEl.style.position = 'absolute'; + this.updateContainerRect(); + this.updatePosition(this.snackBarRef.containerInstance.snackBarConfig); + const snackBarComponent = this; + this.parentScrollSubscription = onParentScrollOrWindowResize(this.parentEl).subscribe(() => { + snackBarComponent.updateContainerRect(); + }); + } + } + + private updatePosition(config: MatSnackBarConfig) { + const isRtl = config.direction === 'rtl'; + const isLeft = (config.horizontalPosition === 'left' || + (config.horizontalPosition === 'start' && !isRtl) || + (config.horizontalPosition === 'end' && isRtl)); + const isRight = !isLeft && config.horizontalPosition !== 'center'; + if (isLeft) { + this.snackBarContainerEl.style.justifyContent = 'flex-start'; + } else if (isRight) { + this.snackBarContainerEl.style.justifyContent = 'flex-end'; + } else { + this.snackBarContainerEl.style.justifyContent = 'center'; + } + if (config.verticalPosition === 'top') { + this.snackBarContainerEl.style.alignItems = 'flex-start'; + } else { + this.snackBarContainerEl.style.alignItems = 'flex-end'; + } + } + + private updateContainerRect() { + const viewportOffset = this.parentEl.getBoundingClientRect(); + this.snackBarContainerEl.style.top = viewportOffset.top + 'px'; + this.snackBarContainerEl.style.left = viewportOffset.left + 'px'; + this.snackBarContainerEl.style.width = viewportOffset.width + 'px'; + this.snackBarContainerEl.style.height = viewportOffset.height + 'px'; } ngOnDestroy() { + if (this.parentScrollSubscription) { + this.parentScrollSubscription.unsubscribe(); + } } action(): void { diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index 32972e4852..80b2b86b33 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -22,7 +22,8 @@ import { EntityInfo } from '@shared/models/entity.models'; import { EntityType } from '@shared/models/entity-type.models'; import { Datasource, DatasourceType } from '@shared/models/widget.models'; import { PageData } from '@shared/models/page/page-data'; -import { isEqual } from '@core/utils'; +import { isDefined, isEqual } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; export enum EntityKeyType { ATTRIBUTE = 'ATTRIBUTE', @@ -63,7 +64,8 @@ export interface EntityKey { export enum EntityKeyValueType { STRING = 'STRING', NUMERIC = 'NUMERIC', - BOOLEAN = 'BOOLEAN' + BOOLEAN = 'BOOLEAN', + DATE_TIME = 'DATE_TIME' } export interface EntityKeyValueTypeData { @@ -93,6 +95,13 @@ export const entityKeyValueTypesMap = new Map { type: FilterPredicateType.COMPLEX, operation: ComplexOperation; - predicates: Array; + predicates: Array; } +export type ComplexFilterPredicate = BaseComplexFilterPredicate; + +export type ComplexFilterPredicateInfo = BaseComplexFilterPredicate; + export type KeyFilterPredicate = StringFilterPredicate | NumericFilterPredicate | BooleanFilterPredicate | - ComplexFilterPredicate; + ComplexFilterPredicate | + ComplexFilterPredicateInfo; + +export interface KeyFilterPredicateUserInfo { + editable: boolean; + label: string; + autogeneratedLabel: boolean; + order?: number; +} + +export interface KeyFilterPredicateInfo { + keyFilterPredicate: KeyFilterPredicate; + userInfo: KeyFilterPredicateUserInfo; +} export interface KeyFilter { key: EntityKey; @@ -243,7 +283,7 @@ export interface KeyFilter { export interface KeyFilterInfo { key: EntityKey; valueType: EntityKeyValueType; - predicates: Array; + predicates: Array; } export interface FilterInfo { @@ -264,7 +304,7 @@ export function filterInfoToKeyFilters(filter: FilterInfo): Array { for (const predicate of keyFilterInfo.predicates) { const keyFilter: KeyFilter = { key, - predicate + predicate: keyFilterPredicateInfoToKeyFilterPredicate(predicate) }; keyFilters.push(keyFilter); } @@ -272,6 +312,112 @@ export function filterInfoToKeyFilters(filter: FilterInfo): Array { return keyFilters; } +export function keyFilterPredicateInfoToKeyFilterPredicate(keyFilterPredicateInfo: KeyFilterPredicateInfo): KeyFilterPredicate { + let keyFilterPredicate = keyFilterPredicateInfo.keyFilterPredicate; + if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) { + const complexInfo = keyFilterPredicate as ComplexFilterPredicateInfo; + const predicates = complexInfo.predicates.map((predicateInfo => keyFilterPredicateInfoToKeyFilterPredicate(predicateInfo))); + keyFilterPredicate = { + type: FilterPredicateType.COMPLEX, + operation: complexInfo.operation, + predicates + } as ComplexFilterPredicate; + } + return keyFilterPredicate; +} + +export function isFilterEditable(filter: FilterInfo): boolean { + if (filter.editable) { + return filter.keyFilters.some(value => isKeyFilterInfoEditable(value)); + } else { + return false; + } +} + +export function isKeyFilterInfoEditable(keyFilterInfo: KeyFilterInfo): boolean { + return keyFilterInfo.predicates.some(value => isPredicateInfoEditable(value)); +} + +export function isPredicateInfoEditable(predicateInfo: KeyFilterPredicateInfo): boolean { + if (predicateInfo.keyFilterPredicate.type === FilterPredicateType.COMPLEX) { + const complexFilterPredicateInfo: ComplexFilterPredicateInfo = predicateInfo.keyFilterPredicate as ComplexFilterPredicateInfo; + return complexFilterPredicateInfo.predicates.some(value => isPredicateInfoEditable(value)); + } else { + return predicateInfo.userInfo.editable; + } +} + +export interface UserFilterInputInfo { + label: string; + valueType: EntityKeyValueType; + info: KeyFilterPredicateInfo; +} + +export function filterToUserFilterInfoList(filter: Filter, translate: TranslateService): Array { + const result = filter.keyFilters.map((keyFilterInfo => keyFilterInfoToUserFilterInfoList(keyFilterInfo, translate))); + let userInputs: Array = [].concat.apply([], result); + userInputs = userInputs.sort((input1, input2) => { + const order1 = isDefined(input1.info.userInfo.order) ? input1.info.userInfo.order : 0; + const order2 = isDefined(input2.info.userInfo.order) ? input2.info.userInfo.order : 0; + return order1 - order2; + }); + return userInputs; +} + +export function keyFilterInfoToUserFilterInfoList(keyFilterInfo: KeyFilterInfo, translate: TranslateService): Array { + const result = keyFilterInfo.predicates.map((predicateInfo => predicateInfoToUserFilterInfoList(keyFilterInfo.key, + keyFilterInfo.valueType, predicateInfo, translate))); + return [].concat.apply([], result); +} + +export function predicateInfoToUserFilterInfoList(key: EntityKey, + valueType: EntityKeyValueType, + predicateInfo: KeyFilterPredicateInfo, + translate: TranslateService): Array { + if (predicateInfo.keyFilterPredicate.type === FilterPredicateType.COMPLEX) { + const complexFilterPredicateInfo: ComplexFilterPredicateInfo = predicateInfo.keyFilterPredicate as ComplexFilterPredicateInfo; + const result = complexFilterPredicateInfo.predicates.map((predicateInfo1 => + predicateInfoToUserFilterInfoList(key, valueType, predicateInfo1, translate))); + return [].concat.apply([], result); + } else { + if (predicateInfo.userInfo.editable) { + const userInput: UserFilterInputInfo = { + info: predicateInfo, + label: predicateInfo.userInfo.label, + valueType + }; + if (predicateInfo.userInfo.autogeneratedLabel) { + userInput.label = generateUserFilterValueLabel(key.key, valueType, + predicateInfo.keyFilterPredicate.operation, translate); + } + return [userInput]; + } else { + return []; + } + } +} + +export function generateUserFilterValueLabel(key: string, valueType: EntityKeyValueType, + operation: StringOperation | BooleanOperation | NumericOperation, + translate: TranslateService) { + let label = key; + let operationTranslationKey: string; + switch (valueType) { + case EntityKeyValueType.STRING: + operationTranslationKey = stringOperationTranslationMap.get(operation as StringOperation); + break; + case EntityKeyValueType.NUMERIC: + case EntityKeyValueType.DATE_TIME: + operationTranslationKey = numericOperationTranslationMap.get(operation as NumericOperation); + break; + case EntityKeyValueType.BOOLEAN: + operationTranslationKey = booleanOperationTranslationMap.get(operation as BooleanOperation); + break; + } + label += ' ' + translate.instant(operationTranslationKey); + return label; +} + export interface Filter extends FilterInfo { id: string; } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index af73d52776..2406c49f98 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1174,18 +1174,18 @@ "filter-required": "Filter is required.", "operation": { "operation": "Operation", - "equal": "Equal", - "not-equal": "Not equal", - "starts-with": "Starts with", - "ends-with": "Ends with", - "contains": "Contains", - "not-contain": "Not contain", - "greater": "Greater than", - "less": "Less than", - "greater-or-equal": "Greater or equal", - "less-or-equal": "Less or equal", - "and": "And", - "or": "Or" + "equal": "equal", + "not-equal": "not equal", + "starts-with": "starts with", + "ends-with": "ends with", + "contains": "contains", + "not-contain": "not contain", + "greater": "greater than", + "less": "less than", + "greater-or-equal": "greater or equal", + "less-or-equal": "less or equal", + "and": "and", + "or": "or" }, "ignore-case": "Ignore case", "value": "Value", @@ -1196,6 +1196,11 @@ "add-complex": "Add complex", "complex-filter": "Complex filter", "edit-complex-filter": "Edit complex filter", + "edit-filter-user-params": "Edit filter predicate user parameters", + "user-parameters": "User parameters", + "display-label": "Label to display", + "autogenerated-label": "Auto generate label", + "order-priority": "Field order priority", "key-filter": "Key filter", "key-filters": "Key filters", "key-name": "Key name", @@ -1210,7 +1215,8 @@ "value-type": "Value type", "string": "String", "numeric": "Numeric", - "boolean": "Boolean" + "boolean": "Boolean", + "date-time": "Datetime" }, "value-type-required": "Key value type is required.", "key-value-type-change-title": "Are you sure you want to change key value type?", @@ -1218,7 +1224,9 @@ "no-key-filters": "No key filters configured", "add-key-filter": "Add key filter", "remove-key-filter": "Remove key filter", - "edit-key-filter": "Edit key filter" + "edit-key-filter": "Edit key filter", + "date": "Date", + "time": "Time" }, "fullscreen": { "expand": "Expand to fullscreen",