57 changed files with 3015 additions and 43 deletions
@ -0,0 +1,30 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div fxLayout="row" [formGroup]="booleanFilterPredicateFormGroup"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>filter.operation.operation</mat-label> |
||||
|
<mat-select required formControlName="operation"> |
||||
|
<mat-option *ngFor="let operation of booleanOperations" [value]="operation"> |
||||
|
{{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-checkbox formControlName="value"> |
||||
|
{{ 'filter.value' | translate }} |
||||
|
</mat-checkbox> |
||||
|
</div> |
||||
@ -0,0 +1,101 @@ |
|||||
|
///
|
||||
|
/// 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, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
||||
|
import { |
||||
|
BooleanFilterPredicate, |
||||
|
BooleanOperation, booleanOperationTranslationMap, |
||||
|
FilterPredicateType |
||||
|
} from '@shared/models/query/query.models'; |
||||
|
import { isDefined } from '@core/utils'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-boolean-filter-predicate', |
||||
|
templateUrl: './boolean-filter-predicate.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => BooleanFilterPredicateComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class BooleanFilterPredicateComponent implements ControlValueAccessor, OnInit { |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
@Input() userMode: boolean; |
||||
|
|
||||
|
booleanFilterPredicateFormGroup: FormGroup; |
||||
|
|
||||
|
booleanOperations = Object.keys(BooleanOperation); |
||||
|
booleanOperationEnum = BooleanOperation; |
||||
|
booleanOperationTranslations = booleanOperationTranslationMap; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
constructor(private fb: FormBuilder) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.booleanFilterPredicateFormGroup = this.fb.group({ |
||||
|
operation: [BooleanOperation.EQUAL, [Validators.required]], |
||||
|
value: [false] |
||||
|
}); |
||||
|
if (this.userMode) { |
||||
|
this.booleanFilterPredicateFormGroup.get('operation').disable({emitEvent: false}); |
||||
|
} |
||||
|
this.booleanFilterPredicateFormGroup.valueChanges.subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState?(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.booleanFilterPredicateFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.booleanFilterPredicateFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(predicate: BooleanFilterPredicate): void { |
||||
|
this.booleanFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); |
||||
|
this.booleanFilterPredicateFormGroup.get('value').patchValue(isDefined(predicate.value) ? predicate.value : false, {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
let predicate: BooleanFilterPredicate = null; |
||||
|
if (this.booleanFilterPredicateFormGroup.valid) { |
||||
|
predicate = this.booleanFilterPredicateFormGroup.getRawValue(); |
||||
|
if (!isDefined(predicate.value)) { |
||||
|
predicate.value = false; |
||||
|
} |
||||
|
predicate.type = FilterPredicateType.BOOLEAN; |
||||
|
} |
||||
|
this.propagateChange(predicate); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<form [formGroup]="complexFilterFormGroup" (ngSubmit)="save()"> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2 translate>filter.complex-filter</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<div mat-dialog-content> |
||||
|
<fieldset [disabled]="isLoading$ | async" fxLayout="column"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>filter.operation.operation</mat-label> |
||||
|
<mat-select required formControlName="operation"> |
||||
|
<mat-option *ngFor="let operation of complexOperations" [value]="operation"> |
||||
|
{{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<tb-filter-predicate-list |
||||
|
[userMode]="data.userMode" |
||||
|
[valueType]="data.valueType" |
||||
|
formControlName="predicates"> |
||||
|
</tb-filter-predicate-list> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayoutAlign="end center"> |
||||
|
<button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid"> |
||||
|
{{ 'action.update' | translate }} |
||||
|
</button> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" |
||||
|
cdkFocusInitial> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,94 @@ |
|||||
|
///
|
||||
|
/// 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, booleanOperationTranslationMap, |
||||
|
ComplexFilterPredicate, ComplexOperation, complexOperationTranslationMap, |
||||
|
EntityKeyValueType, |
||||
|
FilterPredicateType |
||||
|
} from '@shared/models/query/query.models'; |
||||
|
|
||||
|
export interface ComplexFilterPredicateDialogData { |
||||
|
complexPredicate: ComplexFilterPredicate; |
||||
|
userMode: boolean; |
||||
|
disabled: boolean; |
||||
|
valueType: EntityKeyValueType; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-complex-filter-predicate-dialog', |
||||
|
templateUrl: './complex-filter-predicate-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: ComplexFilterPredicateDialogComponent}], |
||||
|
styleUrls: [] |
||||
|
}) |
||||
|
export class ComplexFilterPredicateDialogComponent extends |
||||
|
DialogComponent<ComplexFilterPredicateDialogComponent, ComplexFilterPredicate> |
||||
|
implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
complexFilterFormGroup: FormGroup; |
||||
|
|
||||
|
complexOperations = Object.keys(ComplexOperation); |
||||
|
complexOperationEnum = ComplexOperation; |
||||
|
complexOperationTranslations = complexOperationTranslationMap; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: ComplexFilterPredicateDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<ComplexFilterPredicateDialogComponent, ComplexFilterPredicate>, |
||||
|
private fb: FormBuilder) { |
||||
|
super(store, router, dialogRef); |
||||
|
|
||||
|
this.complexFilterFormGroup = this.fb.group( |
||||
|
{ |
||||
|
operation: [this.data.complexPredicate.operation, [Validators.required]], |
||||
|
predicates: [this.data.complexPredicate.predicates, [Validators.required]] |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
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.complexFilterFormGroup.valid) { |
||||
|
const predicate: ComplexFilterPredicate = this.complexFilterFormGroup.getRawValue(); |
||||
|
predicate.type = FilterPredicateType.COMPLEX; |
||||
|
this.dialogRef.close(predicate); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div fxLayout="row"> |
||||
|
<mat-label translate>filter.complex-filter</mat-label> |
||||
|
<button mat-icon-button color="primary" |
||||
|
[fxShow]="!disabled" |
||||
|
type="button" |
||||
|
(click)="openComplexFilterDialog()" |
||||
|
matTooltip="{{ 'filter.edit-complex-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>edit</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
@ -0,0 +1,97 @@ |
|||||
|
///
|
||||
|
/// 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 { ComplexFilterPredicate, EntityKeyValueType } from '@shared/models/query/query.models'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { |
||||
|
ComplexFilterPredicateDialogComponent, |
||||
|
ComplexFilterPredicateDialogData |
||||
|
} from '@home/components/filter/complex-filter-predicate-dialog.component'; |
||||
|
import { deepClone } from '@core/utils'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-complex-filter-predicate', |
||||
|
templateUrl: './complex-filter-predicate.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => ComplexFilterPredicateComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class ComplexFilterPredicateComponent implements ControlValueAccessor, OnInit { |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
@Input() userMode: boolean; |
||||
|
|
||||
|
@Input() valueType: EntityKeyValueType; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
private complexFilterPredicate: ComplexFilterPredicate; |
||||
|
|
||||
|
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(predicate: ComplexFilterPredicate): void { |
||||
|
this.complexFilterPredicate = predicate; |
||||
|
} |
||||
|
|
||||
|
private openComplexFilterDialog() { |
||||
|
this.dialog.open<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateDialogData, |
||||
|
ComplexFilterPredicate>(ComplexFilterPredicateDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
complexPredicate: deepClone(this.complexFilterPredicate), |
||||
|
disabled: this.disabled, |
||||
|
userMode: this.userMode, |
||||
|
valueType: this.valueType |
||||
|
} |
||||
|
}).afterClosed().subscribe( |
||||
|
(result) => { |
||||
|
if (result) { |
||||
|
this.complexFilterPredicate = result; |
||||
|
this.updateModel(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
this.propagateChange(this.complexFilterPredicate); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="min-width: 480px;"> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2>{{ (isAdd ? 'filter.add' : 'filter.edit') | translate }}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div mat-dialog-content> |
||||
|
<fieldset [disabled]="isLoading$ | async"> |
||||
|
<div fxFlex fxLayout="column"> |
||||
|
<mat-form-field fxFlex class="mat-block"> |
||||
|
<mat-label translate>filter.name</mat-label> |
||||
|
<input matInput formControlName="filter" required> |
||||
|
<mat-error *ngIf="filterFormGroup.get('filter').hasError('required')"> |
||||
|
{{ 'filter.name-required' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="filterFormGroup.get('filter').hasError('duplicateFilterName')"> |
||||
|
{{ 'filter.duplicate-filter' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<tb-key-filter-list |
||||
|
formControlName="keyFilters" |
||||
|
[userMode]="userMode"> |
||||
|
</tb-key-filter-list> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayoutAlign="end center"> |
||||
|
<button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty"> |
||||
|
{{ (isAdd ? 'action.add' : 'action.update') | translate }} |
||||
|
</button> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" cdkFocusInitial> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,137 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
ValidatorFn, |
||||
|
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 { Filter, Filters } from '@shared/models/query/query.models'; |
||||
|
|
||||
|
export interface FilterDialogData { |
||||
|
isAdd: boolean; |
||||
|
userMode: boolean; |
||||
|
filters: Filters | Array<Filter>; |
||||
|
filter?: Filter; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-filter-dialog', |
||||
|
templateUrl: './filter-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: FilterDialogComponent}], |
||||
|
styleUrls: [] |
||||
|
}) |
||||
|
export class FilterDialogComponent extends DialogComponent<FilterDialogComponent, Filter> |
||||
|
implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
isAdd: boolean; |
||||
|
userMode: boolean; |
||||
|
filters: Array<Filter>; |
||||
|
|
||||
|
filter: Filter; |
||||
|
|
||||
|
filterFormGroup: FormGroup; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: FilterDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<FilterDialogComponent, Filter>, |
||||
|
private fb: FormBuilder, |
||||
|
private utils: UtilsService, |
||||
|
public translate: TranslateService) { |
||||
|
super(store, router, dialogRef); |
||||
|
this.isAdd = data.isAdd; |
||||
|
this.userMode = data.userMode; |
||||
|
if (Array.isArray(data.filters)) { |
||||
|
this.filters = data.filters; |
||||
|
} else { |
||||
|
this.filters = []; |
||||
|
for (const filterId of Object.keys(data.filters)) { |
||||
|
this.filters.push(data.filters[filterId]); |
||||
|
} |
||||
|
} |
||||
|
if (this.isAdd && !this.data.filter) { |
||||
|
this.filter = { |
||||
|
id: null, |
||||
|
filter: '', |
||||
|
keyFilters: [] |
||||
|
}; |
||||
|
} else { |
||||
|
this.filter = data.filter; |
||||
|
} |
||||
|
|
||||
|
this.filterFormGroup = this.fb.group({ |
||||
|
filter: [this.filter.filter, [this.validateDuplicateFilterName(), Validators.required]], |
||||
|
keyFilters: [this.filter.keyFilters, Validators.required] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
validateDuplicateFilterName(): ValidatorFn { |
||||
|
return (c: FormControl) => { |
||||
|
const newFilter = c.value; |
||||
|
const found = this.filters.find((filter) => filter.filter === newFilter); |
||||
|
if (found) { |
||||
|
if (this.isAdd || this.filter.id !== found.id) { |
||||
|
return { |
||||
|
duplicateFilterName: { |
||||
|
valid: false |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
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.filter.filter = this.filterFormGroup.get('filter').value; |
||||
|
this.filter.keyFilters = this.filterFormGroup.get('keyFilters').value; |
||||
|
if (this.isAdd) { |
||||
|
this.filter.id = this.utils.guid(); |
||||
|
} |
||||
|
this.dialogRef.close(this.filter); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<section fxLayout="column" [formGroup]="filterListFormGroup"> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" style="max-height: 40px;" |
||||
|
formArrayName="predicates" |
||||
|
*ngFor="let predicateControl of predicatesFormArray().controls; let $index = index"> |
||||
|
<tb-filter-predicate |
||||
|
[userMode]="userMode" |
||||
|
[valueType]="valueType" |
||||
|
[formControl]="predicateControl"> |
||||
|
</tb-filter-predicate> |
||||
|
<button mat-icon-button color="primary" |
||||
|
[fxShow]="!disabled && !userMode" |
||||
|
type="button" |
||||
|
(click)="removePredicate($index)" |
||||
|
matTooltip="{{ 'filter.remove-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
<span [fxShow]="!predicatesFormArray().length" |
||||
|
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}" |
||||
|
class="no-data-found" translate>filter.no-filters</span> |
||||
|
<div style="margin-top: 8px;" fxLayout="row" fxLayoutGap="8px"> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
[fxShow]="!disabled && !userMode" |
||||
|
(click)="addPredicate(false)" |
||||
|
type="button" |
||||
|
matTooltip="{{ 'filter.add-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
{{ 'action.add' | translate }} |
||||
|
</button> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
[fxShow]="!disabled && !userMode" |
||||
|
(click)="addPredicate(true)" |
||||
|
type="button" |
||||
|
matTooltip="{{ 'filter.add-complex-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
{{ 'filter.add-complex' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</section> |
||||
@ -0,0 +1,159 @@ |
|||||
|
///
|
||||
|
/// 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 { |
||||
|
AbstractControl, |
||||
|
ControlValueAccessor, |
||||
|
FormArray, |
||||
|
FormBuilder, |
||||
|
FormGroup, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { Observable, of, Subscription } from 'rxjs'; |
||||
|
import { |
||||
|
ComplexFilterPredicate, |
||||
|
createDefaultFilterPredicate, |
||||
|
EntityKeyValueType, |
||||
|
KeyFilterPredicate |
||||
|
} from '@shared/models/query/query.models'; |
||||
|
import { |
||||
|
ComplexFilterPredicateDialogComponent, |
||||
|
ComplexFilterPredicateDialogData |
||||
|
} from '@home/components/filter/complex-filter-predicate-dialog.component'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-filter-predicate-list', |
||||
|
templateUrl: './filter-predicate-list.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => FilterPredicateListComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class FilterPredicateListComponent implements ControlValueAccessor, OnInit { |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
@Input() userMode: boolean; |
||||
|
|
||||
|
@Input() valueType: EntityKeyValueType; |
||||
|
|
||||
|
filterListFormGroup: FormGroup; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
private valueChangeSubscription: Subscription = null; |
||||
|
|
||||
|
constructor(private fb: FormBuilder, |
||||
|
private dialog: MatDialog) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.filterListFormGroup = this.fb.group({}); |
||||
|
this.filterListFormGroup.addControl('predicates', |
||||
|
this.fb.array([])); |
||||
|
} |
||||
|
|
||||
|
predicatesFormArray(): FormArray { |
||||
|
return this.filterListFormGroup.get('predicates') as FormArray; |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState?(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.filterListFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.filterListFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(predicates: Array<KeyFilterPredicate>): void { |
||||
|
if (this.valueChangeSubscription) { |
||||
|
this.valueChangeSubscription.unsubscribe(); |
||||
|
} |
||||
|
const predicateControls: Array<AbstractControl> = []; |
||||
|
if (predicates) { |
||||
|
for (const predicate of predicates) { |
||||
|
predicateControls.push(this.fb.control(predicate, [Validators.required])); |
||||
|
} |
||||
|
} |
||||
|
this.filterListFormGroup.setControl('predicates', this.fb.array(predicateControls)); |
||||
|
this.valueChangeSubscription = this.filterListFormGroup.valueChanges.subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
if (this.disabled) { |
||||
|
this.filterListFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.filterListFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public removePredicate(index: number) { |
||||
|
(this.filterListFormGroup.get('predicates') as FormArray).removeAt(index); |
||||
|
} |
||||
|
|
||||
|
public addPredicate(complex: boolean) { |
||||
|
const predicatesFormArray = this.filterListFormGroup.get('predicates') as FormArray; |
||||
|
const predicate = createDefaultFilterPredicate(this.valueType, complex); |
||||
|
let observable: Observable<KeyFilterPredicate>; |
||||
|
if (complex) { |
||||
|
observable = this.openComplexFilterDialog(predicate as ComplexFilterPredicate); |
||||
|
} else { |
||||
|
observable = of(predicate); |
||||
|
} |
||||
|
observable.subscribe((result) => { |
||||
|
if (result) { |
||||
|
predicatesFormArray.push(this.fb.control(result, [Validators.required])); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private openComplexFilterDialog(predicate: ComplexFilterPredicate): Observable<KeyFilterPredicate> { |
||||
|
return this.dialog.open<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateDialogData, |
||||
|
ComplexFilterPredicate>(ComplexFilterPredicateDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
complexPredicate: predicate, |
||||
|
disabled: this.disabled, |
||||
|
userMode: this.userMode, |
||||
|
valueType: this.valueType |
||||
|
} |
||||
|
}).afterClosed(); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const predicates: Array<KeyFilterPredicate> = this.filterListFormGroup.getRawValue().predicates; |
||||
|
if (predicates.length) { |
||||
|
this.propagateChange(predicates); |
||||
|
} else { |
||||
|
this.propagateChange(null); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div fxLayout="column" [formGroup]="filterPredicateFormGroup" |
||||
|
[ngSwitch]="type"> |
||||
|
<ng-template [ngSwitchCase]="filterPredicateType.STRING"> |
||||
|
<tb-string-filter-predicate [userMode]="userMode" |
||||
|
formControlName="predicate"> |
||||
|
</tb-string-filter-predicate> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="filterPredicateType.NUMERIC"> |
||||
|
<tb-numeric-filter-predicate [userMode]="userMode" |
||||
|
formControlName="predicate"> |
||||
|
</tb-numeric-filter-predicate> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="filterPredicateType.BOOLEAN"> |
||||
|
<tb-boolean-filter-predicate [userMode]="userMode" |
||||
|
formControlName="predicate"> |
||||
|
</tb-boolean-filter-predicate> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="filterPredicateType.COMPLEX"> |
||||
|
<tb-complex-filter-predicate |
||||
|
[valueType]="valueType" |
||||
|
[userMode]="userMode" |
||||
|
formControlName="predicate"> |
||||
|
</tb-complex-filter-predicate> |
||||
|
</ng-template> |
||||
|
</div> |
||||
@ -0,0 +1,93 @@ |
|||||
|
///
|
||||
|
/// 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, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
||||
|
import { |
||||
|
EntityKeyValueType, |
||||
|
FilterPredicateType, KeyFilterPredicate |
||||
|
} from '@shared/models/query/query.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-filter-predicate', |
||||
|
templateUrl: './filter-predicate.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => FilterPredicateComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class FilterPredicateComponent implements ControlValueAccessor, OnInit { |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
@Input() userMode: boolean; |
||||
|
|
||||
|
@Input() valueType: EntityKeyValueType; |
||||
|
|
||||
|
filterPredicateFormGroup: FormGroup; |
||||
|
|
||||
|
type: FilterPredicateType; |
||||
|
|
||||
|
filterPredicateType = FilterPredicateType; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
constructor(private fb: FormBuilder) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.filterPredicateFormGroup = this.fb.group({ |
||||
|
predicate: [null, [Validators.required]] |
||||
|
}); |
||||
|
this.filterPredicateFormGroup.valueChanges.subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState?(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.filterPredicateFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.filterPredicateFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(predicate: KeyFilterPredicate): void { |
||||
|
this.type = predicate.type; |
||||
|
this.filterPredicateFormGroup.get('predicate').patchValue(predicate, {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
let predicate: KeyFilterPredicate = null; |
||||
|
if (this.filterPredicateFormGroup.valid) { |
||||
|
predicate = this.filterPredicateFormGroup.getRawValue().predicate; |
||||
|
} |
||||
|
this.propagateChange(predicate); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<mat-form-field [floatLabel]="showLabel ? 'auto' : 'always'" |
||||
|
[hideRequiredMarker]="!showLabel" [formGroup]="selectFilterFormGroup" class="mat-block"> |
||||
|
<mat-label *ngIf="!showLabel"></mat-label> |
||||
|
<input matInput type="text" placeholder="{{ 'filter.filter' | translate }}" |
||||
|
#filterInput |
||||
|
formControlName="filter" |
||||
|
(focusin)="onFocus()" |
||||
|
[required]="tbRequired" |
||||
|
(keydown)="filterEnter($event)" |
||||
|
(keypress)="filterEnter($event)" |
||||
|
[matAutocomplete]="filterAutocomplete"> |
||||
|
<button *ngIf="selectFilterFormGroup.get('filter').value && !disabled" |
||||
|
type="button" |
||||
|
matSuffix mat-icon-button aria-label="Clear" |
||||
|
(click)="clear()"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
<mat-autocomplete class="tb-autocomplete" |
||||
|
#filterAutocomplete="matAutocomplete" |
||||
|
[displayWith]="displayFilterFn"> |
||||
|
<mat-option *ngFor="let filter of filteredFilters | async" [value]="filter"> |
||||
|
<span [innerHTML]="filter.filter | highlight:searchText"></span> |
||||
|
</mat-option> |
||||
|
<mat-option *ngIf="!(filteredFilters | async)?.length" [value]="null" class="tb-not-found"> |
||||
|
<div class="tb-not-found-content" (click)="$event.stopPropagation()"> |
||||
|
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty"> |
||||
|
<span translate>filter.no-filters-found</span> |
||||
|
</div> |
||||
|
<ng-template #searchNotEmpty> |
||||
|
<span> |
||||
|
{{ translate.get('filter.no-filter-matching', |
||||
|
{filter: truncate.transform(searchText, true, 6, '...')}) | async }} |
||||
|
</span> |
||||
|
</ng-template> |
||||
|
<span> |
||||
|
<a translate (click)="createFilter($event, searchText)">filter.create-new-filter</a> |
||||
|
</span> |
||||
|
</div> |
||||
|
</mat-option> |
||||
|
</mat-autocomplete> |
||||
|
<mat-error *ngIf="!modelValue && tbRequired"> |
||||
|
{{ 'filter.filter-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
@ -0,0 +1,22 @@ |
|||||
|
///
|
||||
|
/// 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 { Observable } from 'rxjs'; |
||||
|
import { Filter } from '@shared/models/query/query.models'; |
||||
|
|
||||
|
export interface FilterSelectCallbacks { |
||||
|
createFilter: (filter: string) => Observable<Filter>; |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
/** |
||||
|
* 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. |
||||
|
*/ |
||||
|
:host { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.mat-form-field-infix { |
||||
|
border-top: none; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,249 @@ |
|||||
|
///
|
||||
|
/// 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 { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
FormControl, |
||||
|
FormGroup, |
||||
|
FormGroupDirective, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
NgForm |
||||
|
} from '@angular/forms'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
import { map, mergeMap, share, tap } from 'rxjs/operators'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@app/core/core.state'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
||||
|
import { IAliasController } from '@core/api/widget-api.models'; |
||||
|
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; |
||||
|
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; |
||||
|
import { ENTER } from '@angular/cdk/keycodes'; |
||||
|
import { ErrorStateMatcher } from '@angular/material/core'; |
||||
|
import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; |
||||
|
import { Filter } from '@shared/models/query/query.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-filter-select', |
||||
|
templateUrl: './filter-select.component.html', |
||||
|
styleUrls: ['./filter-select.component.scss'], |
||||
|
providers: [{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => FilterSelectComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: ErrorStateMatcher, |
||||
|
useExisting: FilterSelectComponent |
||||
|
}] |
||||
|
}) |
||||
|
export class FilterSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher { |
||||
|
|
||||
|
selectFilterFormGroup: FormGroup; |
||||
|
|
||||
|
modelValue: string | null; |
||||
|
|
||||
|
@Input() |
||||
|
aliasController: IAliasController; |
||||
|
|
||||
|
@Input() |
||||
|
callbacks: FilterSelectCallbacks; |
||||
|
|
||||
|
@Input() |
||||
|
showLabel: boolean; |
||||
|
|
||||
|
@ViewChild('filterAutocomplete') filterAutocomplete: MatAutocomplete; |
||||
|
@ViewChild('autocomplete', { read: MatAutocompleteTrigger }) autoCompleteTrigger: MatAutocompleteTrigger; |
||||
|
|
||||
|
|
||||
|
private requiredValue: boolean; |
||||
|
get tbRequired(): boolean { |
||||
|
return this.requiredValue; |
||||
|
} |
||||
|
@Input() |
||||
|
set tbRequired(value: boolean) { |
||||
|
this.requiredValue = coerceBooleanProperty(value); |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@ViewChild('filterInput', {static: true}) filterInput: ElementRef; |
||||
|
|
||||
|
filterList: Array<Filter> = []; |
||||
|
|
||||
|
filteredFilters: Observable<Array<Filter>>; |
||||
|
|
||||
|
searchText = ''; |
||||
|
|
||||
|
private dirty = false; |
||||
|
|
||||
|
private creatingFilter = false; |
||||
|
|
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
constructor(private store: Store<AppState>, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public translate: TranslateService, |
||||
|
public truncate: TruncatePipe, |
||||
|
private fb: FormBuilder) { |
||||
|
this.selectFilterFormGroup = this.fb.group({ |
||||
|
filter: [null] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
const filters = this.aliasController.getFilters(); |
||||
|
for (const filterId of Object.keys(filters)) { |
||||
|
this.filterList.push(filters[filterId]); |
||||
|
} |
||||
|
|
||||
|
this.filteredFilters = this.selectFilterFormGroup.get('filter').valueChanges |
||||
|
.pipe( |
||||
|
tap(value => { |
||||
|
let modelValue; |
||||
|
if (typeof value === 'string' || !value) { |
||||
|
modelValue = null; |
||||
|
} else { |
||||
|
modelValue = value; |
||||
|
} |
||||
|
this.updateView(modelValue); |
||||
|
if (value === null) { |
||||
|
this.clear(); |
||||
|
} |
||||
|
}), |
||||
|
map(value => value ? (typeof value === 'string' ? value : value.filter) : ''), |
||||
|
mergeMap(name => this.fetchFilters(name) ), |
||||
|
share() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
||||
|
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
||||
|
const customErrorState = this.tbRequired && !this.modelValue; |
||||
|
return originalErrorState || customErrorState; |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void {} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.selectFilterFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.selectFilterFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: string | null): void { |
||||
|
this.searchText = ''; |
||||
|
let filter = null; |
||||
|
if (value != null) { |
||||
|
const filters = this.aliasController.getFilters(); |
||||
|
if (filters[value]) { |
||||
|
filter = filters[value]; |
||||
|
} |
||||
|
} |
||||
|
if (filter != null) { |
||||
|
this.modelValue = filter.id; |
||||
|
this.selectFilterFormGroup.get('filter').patchValue(filter, {emitEvent: false}); |
||||
|
} else { |
||||
|
this.modelValue = null; |
||||
|
this.selectFilterFormGroup.get('filter').patchValue('', {emitEvent: false}); |
||||
|
} |
||||
|
this.dirty = true; |
||||
|
} |
||||
|
|
||||
|
onFocus() { |
||||
|
if (this.dirty) { |
||||
|
this.selectFilterFormGroup.get('filter').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
||||
|
this.dirty = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateView(value: Filter | null) { |
||||
|
const filterId = value ? value.id : null; |
||||
|
if (this.modelValue !== filterId) { |
||||
|
this.modelValue = filterId; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
displayFilterFn(filter?: Filter): string | undefined { |
||||
|
return filter ? filter.filter : undefined; |
||||
|
} |
||||
|
|
||||
|
fetchFilters(searchText?: string): Observable<Array<Filter>> { |
||||
|
this.searchText = searchText; |
||||
|
let result = this.filterList; |
||||
|
if (searchText && searchText.length) { |
||||
|
result = this.filterList.filter((filter) => filter.filter.toLowerCase().includes(searchText.toLowerCase())); |
||||
|
} |
||||
|
return of(result); |
||||
|
} |
||||
|
|
||||
|
clear(value: string = '') { |
||||
|
this.filterInput.nativeElement.value = value; |
||||
|
this.selectFilterFormGroup.get('filter').patchValue(value, {emitEvent: true}); |
||||
|
setTimeout(() => { |
||||
|
this.filterInput.nativeElement.blur(); |
||||
|
this.filterInput.nativeElement.focus(); |
||||
|
}, 0); |
||||
|
} |
||||
|
|
||||
|
textIsNotEmpty(text: string): boolean { |
||||
|
return (text && text != null && text.length > 0) ? true : false; |
||||
|
} |
||||
|
|
||||
|
filterEnter($event: KeyboardEvent) { |
||||
|
if ($event.keyCode === ENTER) { |
||||
|
$event.preventDefault(); |
||||
|
if (!this.modelValue) { |
||||
|
this.createFilter($event, this.searchText); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
createFilter($event: Event, filter: string) { |
||||
|
$event.preventDefault(); |
||||
|
this.creatingFilter = true; |
||||
|
if (this.callbacks && this.callbacks.createFilter) { |
||||
|
this.callbacks.createFilter(filter).subscribe((newFilter) => { |
||||
|
if (!newFilter) { |
||||
|
setTimeout(() => { |
||||
|
this.filterInput.nativeElement.blur(); |
||||
|
this.filterInput.nativeElement.focus(); |
||||
|
}, 0); |
||||
|
} else { |
||||
|
this.filterList.push(newFilter); |
||||
|
this.modelValue = newFilter.id; |
||||
|
this.selectFilterFormGroup.get('filter').patchValue(newFilter, {emitEvent: true}); |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,99 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<form [formGroup]="filtersFormGroup" (ngSubmit)="save()" style="width: 700px;"> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2>{{ title | translate }}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
||||
|
<div mat-dialog-content> |
||||
|
<div class="tb-filters-header" fxLayout="row" fxLayoutAlign="start center"> |
||||
|
<span fxFlex="5"></span> |
||||
|
<div fxFlex="95" fxLayout="row" fxLayoutAlign="start center"> |
||||
|
<span class="tb-header-label" translate fxFlex>filter.filter</span> |
||||
|
<span style="min-width: 80px;"></span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<fieldset [disabled]="isLoading$ | async"> |
||||
|
<mat-divider></mat-divider> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" |
||||
|
formArrayName="filters" |
||||
|
*ngFor="let filterControl of filtersFormArray().controls; let $index = index"> |
||||
|
<span fxFlex="5">{{$index + 1}}.</span> |
||||
|
<div class="mat-elevation-z4 tb-filter" fxFlex="95" fxLayout="row" fxLayoutAlign="start center"> |
||||
|
<mat-form-field floatLabel="always" hideRequiredMarker class="mat-block" fxFlex> |
||||
|
<mat-label></mat-label> |
||||
|
<input matInput [formControl]="filterControl.get('filter')" required placeholder="{{ 'filter.filter' | translate }}"> |
||||
|
<mat-error *ngIf="filterControl.get('filter').hasError('required')"> |
||||
|
{{ 'filter.filter-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<button [disabled]="isLoading$ | async" |
||||
|
mat-icon-button color="primary" |
||||
|
style="min-width: 40px;" |
||||
|
type="button" |
||||
|
(click)="editFilter($index)" |
||||
|
matTooltip="{{ 'filter.edit' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>edit</mat-icon> |
||||
|
</button> |
||||
|
<button [disabled]="isLoading$ | async" |
||||
|
mat-icon-button color="primary" |
||||
|
style="min-width: 40px;" |
||||
|
type="button" |
||||
|
(click)="removeFilter($index)" |
||||
|
matTooltip="{{ 'filter.remove-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions> |
||||
|
<button mat-raised-button color="primary" |
||||
|
type="button" |
||||
|
(click)="addFilter()" |
||||
|
[fxShow]="!disableAdd" |
||||
|
[disabled]="isLoading$ | async" |
||||
|
matTooltip="{{ 'filter.add' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
{{ 'filter.add' | translate }} |
||||
|
</button> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || filtersFormGroup.invalid || !filtersFormGroup.dirty"> |
||||
|
{{ 'action.save' | translate }} |
||||
|
</button> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" |
||||
|
cdkFocusInitial> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,44 @@ |
|||||
|
/** |
||||
|
* 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. |
||||
|
*/ |
||||
|
:host { |
||||
|
|
||||
|
.tb-filters-header { |
||||
|
min-height: 40px; |
||||
|
padding: 0 11px; |
||||
|
margin: 5px; |
||||
|
|
||||
|
.tb-header-label { |
||||
|
font-size: 14px; |
||||
|
color: rgba(0, 0, 0, .570588); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
mat-divider{ |
||||
|
margin: -1px -24px; |
||||
|
} |
||||
|
|
||||
|
.tb-filter { |
||||
|
padding: 0 0 0 10px; |
||||
|
margin: 5px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.mat-dialog-content { |
||||
|
padding-top: 0 !important; |
||||
|
padding-bottom: 0 !important; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,241 @@ |
|||||
|
///
|
||||
|
/// 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, MatDialog, 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 { DatasourceType, Widget } from '@shared/models/widget.models'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { deepClone } from '@core/utils'; |
||||
|
import { Filter, Filters, KeyFilterInfo } from '@shared/models/query/query.models'; |
||||
|
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; |
||||
|
|
||||
|
export interface FiltersDialogData { |
||||
|
filters: Filters; |
||||
|
widgets: Array<Widget>; |
||||
|
isSingleFilter?: boolean; |
||||
|
isSingleWidget?: boolean; |
||||
|
disableAdd?: boolean; |
||||
|
singleFilter?: Filter; |
||||
|
customTitle?: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-filters-dialog', |
||||
|
templateUrl: './filters-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: FiltersDialogComponent}], |
||||
|
styleUrls: ['./filters-dialog.component.scss'] |
||||
|
}) |
||||
|
export class FiltersDialogComponent extends DialogComponent<FiltersDialogComponent, Filters> |
||||
|
implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
title: string; |
||||
|
disableAdd: boolean; |
||||
|
|
||||
|
filterToWidgetsMap: {[filterId: string]: Array<string>} = {}; |
||||
|
|
||||
|
filtersFormGroup: FormGroup; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: FiltersDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<FiltersDialogComponent, Filters>, |
||||
|
private fb: FormBuilder, |
||||
|
private utils: UtilsService, |
||||
|
private translate: TranslateService, |
||||
|
private dialogs: DialogService, |
||||
|
private dialog: MatDialog) { |
||||
|
super(store, router, dialogRef); |
||||
|
this.title = data.customTitle ? data.customTitle : 'filter.filters'; |
||||
|
this.disableAdd = this.data.disableAdd; |
||||
|
|
||||
|
if (data.widgets) { |
||||
|
let widgetsTitleList: Array<string>; |
||||
|
if (this.data.isSingleWidget && this.data.widgets.length === 1) { |
||||
|
const widget = this.data.widgets[0]; |
||||
|
widgetsTitleList = [widget.config.title]; |
||||
|
for (const filterId of Object.keys(this.data.filters)) { |
||||
|
this.filterToWidgetsMap[filterId] = widgetsTitleList; |
||||
|
} |
||||
|
} else { |
||||
|
this.data.widgets.forEach((widget) => { |
||||
|
const datasources = this.utils.validateDatasources(widget.config.datasources); |
||||
|
datasources.forEach((datasource) => { |
||||
|
if (datasource.type === DatasourceType.entity && datasource.filterId) { |
||||
|
widgetsTitleList = this.filterToWidgetsMap[datasource.filterId]; |
||||
|
if (!widgetsTitleList) { |
||||
|
widgetsTitleList = []; |
||||
|
this.filterToWidgetsMap[datasource.filterId] = widgetsTitleList; |
||||
|
} |
||||
|
widgetsTitleList.push(widget.config.title); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
const filterControls: Array<AbstractControl> = []; |
||||
|
for (const filterId of Object.keys(this.data.filters)) { |
||||
|
const filter = this.data.filters[filterId]; |
||||
|
filterControls.push(this.createFilterFormControl(filterId, filter)); |
||||
|
} |
||||
|
|
||||
|
this.filtersFormGroup = this.fb.group({ |
||||
|
filters: this.fb.array(filterControls) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private createFilterFormControl(filterId: string, filter: Filter): AbstractControl { |
||||
|
const filterFormControl = this.fb.group({ |
||||
|
id: [filterId], |
||||
|
filter: [filter ? filter.filter : null, [Validators.required]], |
||||
|
keyFilters: [filter ? filter.keyFilters : [], [Validators.required]] |
||||
|
}); |
||||
|
return filterFormControl; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
filtersFormArray(): FormArray { |
||||
|
return this.filtersFormGroup.get('filters') 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; |
||||
|
} |
||||
|
|
||||
|
removeFilter(index: number) { |
||||
|
const filter = (this.filtersFormGroup.get('filters').value as any[])[index]; |
||||
|
const widgetsTitleList = this.filterToWidgetsMap[filter.id]; |
||||
|
if (widgetsTitleList) { |
||||
|
let widgetsListHtml = ''; |
||||
|
for (const widgetTitle of widgetsTitleList) { |
||||
|
widgetsListHtml += '<br/>\'' + widgetTitle + '\''; |
||||
|
} |
||||
|
const message = this.translate.instant('entity.unable-delete-filter-text', |
||||
|
{filter: filter.filter, widgetsList: widgetsListHtml}); |
||||
|
this.dialogs.alert(this.translate.instant('entity.unable-delete-filter-title'), |
||||
|
message, this.translate.instant('action.close'), true); |
||||
|
} else { |
||||
|
(this.filtersFormGroup.get('filters') as FormArray).removeAt(index); |
||||
|
this.filtersFormGroup.markAsDirty(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public addFilter() { |
||||
|
this.openFilterDialog(-1); |
||||
|
} |
||||
|
|
||||
|
public editFilter(index: number) { |
||||
|
this.openFilterDialog(index); |
||||
|
} |
||||
|
|
||||
|
private openFilterDialog(index: number) { |
||||
|
const isAdd = index === -1; |
||||
|
let filter; |
||||
|
const filtersArray = this.filtersFormGroup.get('filters').value as any[]; |
||||
|
if (!isAdd) { |
||||
|
filter = filtersArray[index]; |
||||
|
} |
||||
|
this.dialog.open<FilterDialogComponent, FilterDialogData, |
||||
|
Filter>(FilterDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
isAdd, |
||||
|
filters: filtersArray, |
||||
|
filter: isAdd ? null : deepClone(filter), |
||||
|
userMode: false |
||||
|
} |
||||
|
}).afterClosed().subscribe((result) => { |
||||
|
if (result) { |
||||
|
if (isAdd) { |
||||
|
(this.filtersFormGroup.get('filters') as FormArray) |
||||
|
.push(this.createFilterFormControl(result.id, result)); |
||||
|
} else { |
||||
|
const filterFormControl = (this.filtersFormGroup.get('filters') as FormArray).at(index); |
||||
|
filterFormControl.get('filter').patchValue(filter.filter); |
||||
|
filterFormControl.get('keyFilters').patchValue(filter.keyFilters); |
||||
|
} |
||||
|
this.filtersFormGroup.markAsDirty(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.submitted = true; |
||||
|
const filters: Filters = {}; |
||||
|
const uniqueFilterList: {[filter: string]: string} = {}; |
||||
|
|
||||
|
let valid = true; |
||||
|
let message: string; |
||||
|
|
||||
|
const filtersArray = this.filtersFormGroup.get('filters').value as any[]; |
||||
|
for (const filterValue of filtersArray) { |
||||
|
const filterId: string = filterValue.id; |
||||
|
const filter: string = filterValue.filter; |
||||
|
const keyFilters: Array<KeyFilterInfo> = filterValue.keyFilters; |
||||
|
if (uniqueFilterList[filter]) { |
||||
|
valid = false; |
||||
|
message = this.translate.instant('filter.duplicate-filter-error', {filter}); |
||||
|
break; |
||||
|
} else if (!keyFilters || !keyFilters.length) { |
||||
|
valid = false; |
||||
|
message = this.translate.instant('filter.missing-key-filters-error', {filter}); |
||||
|
break; |
||||
|
} else { |
||||
|
uniqueFilterList[filter] = filter; |
||||
|
filters[filterId] = {id: filterId, filter, keyFilters}; |
||||
|
} |
||||
|
} |
||||
|
if (valid) { |
||||
|
this.dialogRef.close(filters); |
||||
|
} else { |
||||
|
this.store.dispatch(new ActionNotificationShow( |
||||
|
{ |
||||
|
message, |
||||
|
type: 'error' |
||||
|
})); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,86 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<form [formGroup]="keyFilterFormGroup" (ngSubmit)="save()"> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2 translate>{{data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter'}}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<div mat-dialog-content> |
||||
|
<fieldset [disabled]="isLoading$ | async" fxLayout="column"> |
||||
|
<section fxLayout="row" fxLayoutGap="8px"> |
||||
|
<section fxFlex="60" fxLayout="row" formGroupName="key" fxLayoutGap="8px"> |
||||
|
<mat-form-field fxFlex="40" class="mat-block"> |
||||
|
<mat-label translate>filter.key-name</mat-label> |
||||
|
<input matInput required formControlName="key"> |
||||
|
<mat-error *ngIf="keyFilterFormGroup.get('key.key').hasError('required')"> |
||||
|
{{ 'filter.key-name-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex="60" class="mat-block"> |
||||
|
<mat-label translate>filter.key-type.key-type</mat-label> |
||||
|
<mat-select required formControlName="type"> |
||||
|
<mat-option *ngFor="let type of entityKeyTypes" [value]="type"> |
||||
|
{{entityKeyTypeTranslations.get(type) | translate}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</section> |
||||
|
<mat-form-field fxFlex="40" class="mat-block"> |
||||
|
<mat-label translate>filter.value-type.value-type</mat-label> |
||||
|
<mat-select matInput formControlName="valueType"> |
||||
|
<mat-select-trigger> |
||||
|
<mat-icon svgIcon="{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.icon }}"></mat-icon> |
||||
|
<span>{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }}</span> |
||||
|
</mat-select-trigger> |
||||
|
<mat-option *ngFor="let valueType of entityKeyValueTypesKeys" [value]="valueType"> |
||||
|
<mat-icon svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon> |
||||
|
<span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span> |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
<mat-error *ngIf="keyFilterFormGroup.get('valueType').hasError('required')"> |
||||
|
{{ 'filter.value-type-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</section> |
||||
|
<tb-filter-predicate-list *ngIf="keyFilterFormGroup.get('valueType').value" |
||||
|
[userMode]="data.userMode" |
||||
|
[valueType]="keyFilterFormGroup.get('valueType').value" |
||||
|
formControlName="predicates"> |
||||
|
</tb-filter-predicate-list> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayoutAlign="end center"> |
||||
|
<button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid"> |
||||
|
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }} |
||||
|
</button> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" |
||||
|
cdkFocusInitial> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,105 @@ |
|||||
|
///
|
||||
|
/// 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 { |
||||
|
EntityKeyType, |
||||
|
entityKeyTypeTranslationMap, |
||||
|
EntityKeyValueType, |
||||
|
entityKeyValueTypesMap, |
||||
|
KeyFilterInfo |
||||
|
} from '@shared/models/query/query.models'; |
||||
|
|
||||
|
export interface KeyFilterDialogData { |
||||
|
keyFilter: KeyFilterInfo; |
||||
|
userMode: boolean; |
||||
|
isAdd: boolean; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-key-filter-dialog', |
||||
|
templateUrl: './key-filter-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: KeyFilterDialogComponent}], |
||||
|
styleUrls: [] |
||||
|
}) |
||||
|
export class KeyFilterDialogComponent extends |
||||
|
DialogComponent<KeyFilterDialogComponent, KeyFilterInfo> |
||||
|
implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
keyFilterFormGroup: FormGroup; |
||||
|
|
||||
|
entityKeyTypes = [EntityKeyType.ENTITY_FIELD, EntityKeyType.ATTRIBUTE, EntityKeyType.TIME_SERIES]; |
||||
|
|
||||
|
entityKeyTypeTranslations = entityKeyTypeTranslationMap; |
||||
|
|
||||
|
entityKeyValueTypesKeys = Object.keys(EntityKeyValueType); |
||||
|
|
||||
|
entityKeyValueTypeEnum = EntityKeyValueType; |
||||
|
|
||||
|
entityKeyValueTypes = entityKeyValueTypesMap; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: KeyFilterDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<KeyFilterDialogComponent, KeyFilterInfo>, |
||||
|
private fb: FormBuilder) { |
||||
|
super(store, router, dialogRef); |
||||
|
|
||||
|
this.keyFilterFormGroup = this.fb.group( |
||||
|
{ |
||||
|
key: this.fb.group( |
||||
|
{ |
||||
|
type: [this.data.keyFilter.key.type, [Validators.required]], |
||||
|
key: [this.data.keyFilter.key.key, [Validators.required]] |
||||
|
} |
||||
|
), |
||||
|
valueType: [this.data.keyFilter.valueType, [Validators.required]], |
||||
|
predicates: [this.data.keyFilter.predicates, [Validators.required]] |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
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.keyFilterFormGroup.valid) { |
||||
|
const keyFilter: KeyFilterInfo = this.keyFilterFormGroup.getRawValue(); |
||||
|
this.dialogRef.close(keyFilter); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<section fxLayout="column" [formGroup]="keyFilterListFormGroup"> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" style="max-height: 40px;" |
||||
|
formArrayName="keyFilters" |
||||
|
*ngFor="let keyFilterControl of keyFiltersFormArray().controls; let $index = index"> |
||||
|
<span>{{ keyFilterControl.value.key.key }}</span> |
||||
|
<span>{{ keyFilterControl.value.key.type }}</span> |
||||
|
<button mat-icon-button color="primary" |
||||
|
type="button" |
||||
|
(click)="editKeyFilter($index)" |
||||
|
matTooltip="{{ 'filter.edit-key-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>edit</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button color="primary" |
||||
|
[fxShow]="!disabled && !userMode" |
||||
|
type="button" |
||||
|
(click)="removeKeyFilter($index)" |
||||
|
matTooltip="{{ 'filter.remove-key-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
<span [fxShow]="!keyFiltersFormArray().length" |
||||
|
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}" |
||||
|
class="no-data-found" translate>filter.no-key-filters</span> |
||||
|
<div style="margin-top: 8px;"> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
[fxShow]="!disabled && !userMode" |
||||
|
(click)="addKeyFilter()" |
||||
|
type="button" |
||||
|
matTooltip="{{ 'filter.add-key-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
{{ 'filter.add-key-filter' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</section> |
||||
@ -0,0 +1,165 @@ |
|||||
|
///
|
||||
|
/// 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 { |
||||
|
AbstractControl, |
||||
|
ControlValueAccessor, |
||||
|
FormArray, |
||||
|
FormBuilder, |
||||
|
FormGroup, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { Observable, Subscription } from 'rxjs'; |
||||
|
import { EntityKeyType, KeyFilterInfo } 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'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-key-filter-list', |
||||
|
templateUrl: './key-filter-list.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => KeyFilterListComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class KeyFilterListComponent implements ControlValueAccessor, OnInit { |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
@Input() userMode: boolean; |
||||
|
|
||||
|
keyFilterListFormGroup: FormGroup; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
private valueChangeSubscription: Subscription = null; |
||||
|
|
||||
|
constructor(private fb: FormBuilder, |
||||
|
private dialog: MatDialog) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.keyFilterListFormGroup = this.fb.group({}); |
||||
|
this.keyFilterListFormGroup.addControl('keyFilters', |
||||
|
this.fb.array([])); |
||||
|
} |
||||
|
|
||||
|
keyFiltersFormArray(): FormArray { |
||||
|
return this.keyFilterListFormGroup.get('keyFilters') as FormArray; |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState?(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.keyFilterListFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.keyFilterListFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(keyFilters: Array<KeyFilterInfo>): void { |
||||
|
if (this.valueChangeSubscription) { |
||||
|
this.valueChangeSubscription.unsubscribe(); |
||||
|
} |
||||
|
const keyFilterControls: Array<AbstractControl> = []; |
||||
|
if (keyFilters) { |
||||
|
for (const keyFilter of keyFilters) { |
||||
|
keyFilterControls.push(this.fb.control(keyFilter, [Validators.required])); |
||||
|
} |
||||
|
} |
||||
|
this.keyFilterListFormGroup.setControl('keyFilters', this.fb.array(keyFilterControls)); |
||||
|
this.valueChangeSubscription = this.keyFilterListFormGroup.valueChanges.subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
if (this.disabled) { |
||||
|
this.keyFilterListFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.keyFilterListFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public removeKeyFilter(index: number) { |
||||
|
(this.keyFilterListFormGroup.get('keyFilters') as FormArray).removeAt(index); |
||||
|
} |
||||
|
|
||||
|
public addKeyFilter() { |
||||
|
const keyFiltersFormArray = this.keyFilterListFormGroup.get('keyFilters') as FormArray; |
||||
|
this.openKeyFilterDialog(null).subscribe((result) => { |
||||
|
if (result) { |
||||
|
keyFiltersFormArray.push(this.fb.control(result, [Validators.required])); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public editKeyFilter(index: number) { |
||||
|
const keyFilter: KeyFilterInfo = |
||||
|
(this.keyFilterListFormGroup.get('keyFilters') as FormArray).at(index).value; |
||||
|
this.openKeyFilterDialog(keyFilter).subscribe( |
||||
|
(result) => { |
||||
|
if (result) { |
||||
|
(this.keyFilterListFormGroup.get('keyFilters') as FormArray).at(index).patchValue(result); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private openKeyFilterDialog(keyFilter?: KeyFilterInfo): Observable<KeyFilterInfo> { |
||||
|
const isAdd = !keyFilter; |
||||
|
if (!keyFilter) { |
||||
|
keyFilter = { |
||||
|
key: { |
||||
|
key: '', |
||||
|
type: EntityKeyType.ATTRIBUTE |
||||
|
}, |
||||
|
valueType: null, |
||||
|
predicates: [] |
||||
|
}; |
||||
|
} |
||||
|
return this.dialog.open<KeyFilterDialogComponent, KeyFilterDialogData, |
||||
|
KeyFilterInfo>(KeyFilterDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
keyFilter: keyFilter ? deepClone(keyFilter): null, |
||||
|
userMode: this.userMode, |
||||
|
isAdd |
||||
|
} |
||||
|
}).afterClosed(); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const keyFilters: Array<KeyFilterInfo> = this.keyFilterListFormGroup.getRawValue().keyFilters; |
||||
|
if (keyFilters.length) { |
||||
|
this.propagateChange(keyFilters); |
||||
|
} else { |
||||
|
this.propagateChange(null); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div fxLayout="row" [formGroup]="numericFilterPredicateFormGroup"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>filter.operation.operation</mat-label> |
||||
|
<mat-select required formControlName="operation"> |
||||
|
<mat-option *ngFor="let operation of numericOperations" [value]="operation"> |
||||
|
{{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>filter.value</mat-label> |
||||
|
<input required type="number" matInput formControlName="value"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
@ -0,0 +1,99 @@ |
|||||
|
///
|
||||
|
/// 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, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
||||
|
import { |
||||
|
FilterPredicateType, NumericFilterPredicate, NumericOperation, numericOperationTranslationMap, |
||||
|
} from '@shared/models/query/query.models'; |
||||
|
import { isDefined } from '@core/utils'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-numeric-filter-predicate', |
||||
|
templateUrl: './numeric-filter-predicate.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => NumericFilterPredicateComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class NumericFilterPredicateComponent implements ControlValueAccessor, OnInit { |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
@Input() userMode: boolean; |
||||
|
|
||||
|
numericFilterPredicateFormGroup: FormGroup; |
||||
|
|
||||
|
numericOperations = Object.keys(NumericOperation); |
||||
|
numericOperationEnum = NumericOperation; |
||||
|
numericOperationTranslations = numericOperationTranslationMap; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
constructor(private fb: FormBuilder) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.numericFilterPredicateFormGroup = this.fb.group({ |
||||
|
operation: [NumericOperation.EQUAL, [Validators.required]], |
||||
|
value: [0, [Validators.required]] |
||||
|
}); |
||||
|
if (this.userMode) { |
||||
|
this.numericFilterPredicateFormGroup.get('operation').disable({emitEvent: false}); |
||||
|
} |
||||
|
this.numericFilterPredicateFormGroup.valueChanges.subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState?(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.numericFilterPredicateFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.numericFilterPredicateFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(predicate: NumericFilterPredicate): void { |
||||
|
this.numericFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); |
||||
|
this.numericFilterPredicateFormGroup.get('value').patchValue(isDefined(predicate.value) ? predicate.value : 0, {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
let predicate: NumericFilterPredicate = null; |
||||
|
if (this.numericFilterPredicateFormGroup.valid) { |
||||
|
predicate = this.numericFilterPredicateFormGroup.getRawValue(); |
||||
|
if (!predicate.value) { |
||||
|
predicate.value = 0; |
||||
|
} |
||||
|
predicate.type = FilterPredicateType.NUMERIC; |
||||
|
} |
||||
|
this.propagateChange(predicate); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div fxLayout="row" [formGroup]="stringFilterPredicateFormGroup"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>filter.operation.operation</mat-label> |
||||
|
<mat-select required formControlName="operation"> |
||||
|
<mat-option *ngFor="let operation of stringOperations" [value]="operation"> |
||||
|
{{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-checkbox formControlName="ignoreCase"> |
||||
|
{{ 'filter.ignore-case' | translate }} |
||||
|
</mat-checkbox> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>filter.value</mat-label> |
||||
|
<input matInput formControlName="value"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
@ -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, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
||||
|
import { |
||||
|
FilterPredicateType, |
||||
|
StringFilterPredicate, |
||||
|
StringOperation, |
||||
|
stringOperationTranslationMap |
||||
|
} from '@shared/models/query/query.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-string-filter-predicate', |
||||
|
templateUrl: './string-filter-predicate.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => StringFilterPredicateComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class StringFilterPredicateComponent implements ControlValueAccessor, OnInit { |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
@Input() userMode: boolean; |
||||
|
|
||||
|
stringFilterPredicateFormGroup: FormGroup; |
||||
|
|
||||
|
stringOperations = Object.keys(StringOperation); |
||||
|
stringOperationEnum = StringOperation; |
||||
|
stringOperationTranslations = stringOperationTranslationMap; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
constructor(private fb: FormBuilder) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.stringFilterPredicateFormGroup = this.fb.group({ |
||||
|
operation: [StringOperation.STARTS_WITH, [Validators.required]], |
||||
|
value: [''], |
||||
|
ignoreCase: [false] |
||||
|
}); |
||||
|
if (this.userMode) { |
||||
|
this.stringFilterPredicateFormGroup.get('operation').disable({emitEvent: false}); |
||||
|
this.stringFilterPredicateFormGroup.get('ignoreCase').disable({emitEvent: false}); |
||||
|
} |
||||
|
this.stringFilterPredicateFormGroup.valueChanges.subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState?(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.stringFilterPredicateFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.stringFilterPredicateFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(predicate: StringFilterPredicate): void { |
||||
|
this.stringFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); |
||||
|
this.stringFilterPredicateFormGroup.get('value').patchValue(predicate.value ? predicate.value : '', {emitEvent: false}); |
||||
|
this.stringFilterPredicateFormGroup.get('ignoreCase').patchValue(predicate.ignoreCase, {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
let predicate: StringFilterPredicate = null; |
||||
|
if (this.stringFilterPredicateFormGroup.valid) { |
||||
|
predicate = this.stringFilterPredicateFormGroup.getRawValue(); |
||||
|
if (!predicate.value) { |
||||
|
predicate.value = ''; |
||||
|
} |
||||
|
predicate.type = FilterPredicateType.STRING; |
||||
|
} |
||||
|
this.propagateChange(predicate); |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue