Commit 25dae17671511c1fd3a51183c3009b0138ce5e28

Authored by Igor Kulikov
1 parent 9600cc04

UI: Dashboard filters implementation

Showing 48 changed files with 856 additions and 173 deletions
@@ -24,7 +24,7 @@ import { EntityAliases } from '@shared/models/alias.models'; @@ -24,7 +24,7 @@ import { EntityAliases } from '@shared/models/alias.models';
24 import { EntityInfo } from '@shared/models/entity.models'; 24 import { EntityInfo } from '@shared/models/entity.models';
25 import { map, mergeMap } from 'rxjs/operators'; 25 import { map, mergeMap } from 'rxjs/operators';
26 import { 26 import {
27 - defaultEntityDataPageLink, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, 27 + defaultEntityDataPageLink, Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink,
28 updateDatasourceFromEntityInfo 28 updateDatasourceFromEntityInfo
29 } from '@shared/models/query/query.models'; 29 } from '@shared/models/query/query.models';
30 30
@@ -41,6 +41,7 @@ export class AliasController implements IAliasController { @@ -41,6 +41,7 @@ export class AliasController implements IAliasController {
41 41
42 entityAliases: EntityAliases; 42 entityAliases: EntityAliases;
43 filters: Filters; 43 filters: Filters;
  44 + userFilters: Filters;
44 45
45 resolvedAliases: {[aliasId: string]: AliasInfo} = {}; 46 resolvedAliases: {[aliasId: string]: AliasInfo} = {};
46 resolvedAliasesObservable: {[aliasId: string]: Observable<AliasInfo>} = {}; 47 resolvedAliasesObservable: {[aliasId: string]: Observable<AliasInfo>} = {};
@@ -54,6 +55,7 @@ export class AliasController implements IAliasController { @@ -54,6 +55,7 @@ export class AliasController implements IAliasController {
54 private origFilters: Filters) { 55 private origFilters: Filters) {
55 this.entityAliases = deepClone(this.origEntityAliases); 56 this.entityAliases = deepClone(this.origEntityAliases);
56 this.filters = deepClone(this.origFilters); 57 this.filters = deepClone(this.origFilters);
  58 + this.userFilters = {};
57 } 59 }
58 60
59 updateEntityAliases(newEntityAliases: EntityAliases) { 61 updateEntityAliases(newEntityAliases: EntityAliases) {
@@ -94,6 +96,9 @@ export class AliasController implements IAliasController { @@ -94,6 +96,9 @@ export class AliasController implements IAliasController {
94 } 96 }
95 this.filters = deepClone(newFilters); 97 this.filters = deepClone(newFilters);
96 if (changedFilterIds.length) { 98 if (changedFilterIds.length) {
  99 + for (const filterId of changedFilterIds) {
  100 + delete this.userFilters[filterId];
  101 + }
97 this.filtersChangedSubject.next(changedFilterIds); 102 this.filtersChangedSubject.next(changedFilterIds);
98 } 103 }
99 } 104 }
@@ -146,7 +151,11 @@ export class AliasController implements IAliasController { @@ -146,7 +151,11 @@ export class AliasController implements IAliasController {
146 } 151 }
147 152
148 getFilterInfo(filterId: string): FilterInfo { 153 getFilterInfo(filterId: string): FilterInfo {
149 - return this.filters[filterId]; 154 + if (this.userFilters[filterId]) {
  155 + return this.userFilters[filterId];
  156 + } else {
  157 + return this.filters[filterId];
  158 + }
150 } 159 }
151 160
152 getKeyFilters(filterId: string): Array<KeyFilter> { 161 getKeyFilters(filterId: string): Array<KeyFilter> {
@@ -353,4 +362,15 @@ export class AliasController implements IAliasController { @@ -353,4 +362,15 @@ export class AliasController implements IAliasController {
353 } 362 }
354 } 363 }
355 } 364 }
  365 +
  366 + updateUserFilter(filter: Filter) {
  367 + let prevUserFilter = this.userFilters[filter.id];
  368 + if (!prevUserFilter) {
  369 + prevUserFilter = this.filters[filter.id];
  370 + }
  371 + if (prevUserFilter && !isEqual(prevUserFilter, filter)) {
  372 + this.userFilters[filter.id] = filter;
  373 + this.filtersChangedSubject.next([filter.id]);
  374 + }
  375 + }
356 } 376 }
@@ -68,6 +68,7 @@ export interface EntityDataSubscriptionOptions { @@ -68,6 +68,7 @@ export interface EntityDataSubscriptionOptions {
68 isPaginatedDataSubscription?: boolean; 68 isPaginatedDataSubscription?: boolean;
69 pageLink?: EntityDataPageLink; 69 pageLink?: EntityDataPageLink;
70 keyFilters?: Array<KeyFilter>; 70 keyFilters?: Array<KeyFilter>;
  71 + additionalKeyFilters?: Array<KeyFilter>;
71 subscriptionTimewindow?: SubscriptionTimewindow; 72 subscriptionTimewindow?: SubscriptionTimewindow;
72 } 73 }
73 74
@@ -206,10 +207,19 @@ export class EntityDataSubscription { @@ -206,10 +207,19 @@ export class EntityDataSubscription {
206 this.subscriber = new TelemetrySubscriber(this.telemetryService); 207 this.subscriber = new TelemetrySubscriber(this.telemetryService);
207 this.dataCommand = new EntityDataCmd(); 208 this.dataCommand = new EntityDataCmd();
208 209
  210 + let keyFilters = this.entityDataSubscriptionOptions.keyFilters;
  211 + if (this.entityDataSubscriptionOptions.additionalKeyFilters) {
  212 + if (keyFilters) {
  213 + keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters);
  214 + } else {
  215 + keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters;
  216 + }
  217 + }
  218 +
209 this.dataCommand.query = { 219 this.dataCommand.query = {
210 entityFilter: this.entityDataSubscriptionOptions.entityFilter, 220 entityFilter: this.entityDataSubscriptionOptions.entityFilter,
211 pageLink: this.entityDataSubscriptionOptions.pageLink, 221 pageLink: this.entityDataSubscriptionOptions.pageLink,
212 - keyFilters: this.entityDataSubscriptionOptions.keyFilters, 222 + keyFilters,
213 entityFields, 223 entityFields,
214 latestValues: this.latestValues 224 latestValues: this.latestValues
215 }; 225 };
@@ -65,7 +65,7 @@ export class EntityDataService { @@ -65,7 +65,7 @@ export class EntityDataService {
65 return of(null); 65 return of(null);
66 } 66 }
67 listener.subscription = this.createSubscription(listener, 67 listener.subscription = this.createSubscription(listener,
68 - datasource.pageLink, datasource.keyFilters, 68 + datasource.pageLink, datasource.keyFilters, null,
69 false); 69 false);
70 return listener.subscription.subscribe(); 70 return listener.subscription.subscribe();
71 } 71 }
@@ -86,15 +86,8 @@ export class EntityDataService { @@ -86,15 +86,8 @@ export class EntityDataService {
86 if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !pageLink)) { 86 if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !pageLink)) {
87 return of(null); 87 return of(null);
88 } 88 }
89 - if (datasource.keyFilters) {  
90 - if (keyFilters) {  
91 - keyFilters = keyFilters.concat(datasource.keyFilters);  
92 - } else {  
93 - keyFilters = datasource.keyFilters;  
94 - }  
95 - }  
96 listener.subscription = this.createSubscription(listener, 89 listener.subscription = this.createSubscription(listener,
97 - pageLink, keyFilters, true); 90 + pageLink, datasource.keyFilters, keyFilters,true);
98 if (listener.subscriptionType === widgetType.timeseries) { 91 if (listener.subscriptionType === widgetType.timeseries) {
99 listener.subscription.entityDataSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); 92 listener.subscription.entityDataSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
100 } 93 }
@@ -110,6 +103,7 @@ export class EntityDataService { @@ -110,6 +103,7 @@ export class EntityDataService {
110 private createSubscription(listener: EntityDataListener, 103 private createSubscription(listener: EntityDataListener,
111 pageLink: EntityDataPageLink, 104 pageLink: EntityDataPageLink,
112 keyFilters: KeyFilter[], 105 keyFilters: KeyFilter[],
  106 + additionalKeyFilters: KeyFilter[],
113 isPaginatedDataSubscription: boolean): EntityDataSubscription { 107 isPaginatedDataSubscription: boolean): EntityDataSubscription {
114 const datasource = listener.configDatasource; 108 const datasource = listener.configDatasource;
115 const subscriptionDataKeys: Array<SubscriptionDataKey> = []; 109 const subscriptionDataKeys: Array<SubscriptionDataKey> = [];
@@ -131,6 +125,7 @@ export class EntityDataService { @@ -131,6 +125,7 @@ export class EntityDataService {
131 entityDataSubscriptionOptions.entityFilter = datasource.entityFilter; 125 entityDataSubscriptionOptions.entityFilter = datasource.entityFilter;
132 entityDataSubscriptionOptions.pageLink = pageLink; 126 entityDataSubscriptionOptions.pageLink = pageLink;
133 entityDataSubscriptionOptions.keyFilters = keyFilters; 127 entityDataSubscriptionOptions.keyFilters = keyFilters;
  128 + entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters;
134 } 129 }
135 entityDataSubscriptionOptions.isPaginatedDataSubscription = isPaginatedDataSubscription; 130 entityDataSubscriptionOptions.isPaginatedDataSubscription = isPaginatedDataSubscription;
136 return new EntityDataSubscription(entityDataSubscriptionOptions, 131 return new EntityDataSubscription(entityDataSubscriptionOptions,
@@ -112,6 +112,7 @@ export interface IAliasController { @@ -112,6 +112,7 @@ export interface IAliasController {
112 getFilterInfo(filterId: string): FilterInfo; 112 getFilterInfo(filterId: string): FilterInfo;
113 getKeyFilters(filterId: string): Array<KeyFilter>; 113 getKeyFilters(filterId: string): Array<KeyFilter>;
114 updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo); 114 updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo);
  115 + updateUserFilter(filter: Filter);
115 updateEntityAliases(entityAliases: EntityAliases); 116 updateEntityAliases(entityAliases: EntityAliases);
116 updateFilters(filters: Filters); 117 updateFilters(filters: Filters);
117 updateAliases(aliasIds?: Array<string>); 118 updateAliases(aliasIds?: Array<string>);
@@ -1011,7 +1011,7 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -1011,7 +1011,7 @@ export class WidgetSubscription implements IWidgetSubscription {
1011 const entityDataListener = this.entityDataListeners[datasourceIndex]; 1011 const entityDataListener = this.entityDataListeners[datasourceIndex];
1012 if (entityDataListener) { 1012 if (entityDataListener) {
1013 const pageLink = entityDataListener.subscription.entityDataSubscriptionOptions.pageLink; 1013 const pageLink = entityDataListener.subscription.entityDataSubscriptionOptions.pageLink;
1014 - const keyFilters = entityDataListener.subscription.entityDataSubscriptionOptions.keyFilters; 1014 + const keyFilters = entityDataListener.subscription.entityDataSubscriptionOptions.additionalKeyFilters;
1015 this.subscribeForPaginatedData(datasourceIndex, pageLink, keyFilters); 1015 this.subscribeForPaginatedData(datasourceIndex, pageLink, keyFilters);
1016 } 1016 }
1017 } 1017 }
@@ -410,7 +410,8 @@ export class ItemBufferService { @@ -410,7 +410,8 @@ export class ItemBufferService {
410 private prepareFilterInfo(filter: Filter): FilterInfo { 410 private prepareFilterInfo(filter: Filter): FilterInfo {
411 return { 411 return {
412 filter: filter.filter, 412 filter: filter.filter,
413 - keyFilters: filter.keyFilters 413 + keyFilters: filter.keyFilters,
  414 + editable: filter.editable
414 }; 415 };
415 } 416 }
416 417
@@ -513,7 +514,8 @@ export class ItemBufferService { @@ -513,7 +514,8 @@ export class ItemBufferService {
513 if (!newFilterId) { 514 if (!newFilterId) {
514 const newFilterName = this.createFilterName(filters, filterInfo.filter); 515 const newFilterName = this.createFilterName(filters, filterInfo.filter);
515 newFilterId = this.utils.guid(); 516 newFilterId = this.utils.guid();
516 - filters[newFilterId] = {id: newFilterId, filter: newFilterName, keyFilters: filterInfo.keyFilters}; 517 + filters[newFilterId] = {id: newFilterId, filter: newFilterName,
  518 + keyFilters: filterInfo.keyFilters, editable: filterInfo.editable};
517 } 519 }
518 return newFilterId; 520 return newFilterId;
519 } 521 }
@@ -15,16 +15,16 @@ @@ -15,16 +15,16 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div fxLayout="row" [formGroup]="booleanFilterPredicateFormGroup">  
19 - <mat-form-field class="mat-block">  
20 - <mat-label translate>filter.operation.operation</mat-label>  
21 - <mat-select required formControlName="operation"> 18 +<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="booleanFilterPredicateFormGroup">
  19 + <mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
  20 + <mat-label></mat-label>
  21 + <mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
22 <mat-option *ngFor="let operation of booleanOperations" [value]="operation"> 22 <mat-option *ngFor="let operation of booleanOperations" [value]="operation">
23 {{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} 23 {{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}}
24 </mat-option> 24 </mat-option>
25 </mat-select> 25 </mat-select>
26 </mat-form-field> 26 </mat-form-field>
27 - <mat-checkbox formControlName="value">  
28 - {{ 'filter.value' | translate }} 27 + <mat-checkbox fxFlex formControlName="value">
  28 + {{ (booleanFilterPredicateFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }}
29 </mat-checkbox> 29 </mat-checkbox>
30 </div> 30 </div>
@@ -26,7 +26,7 @@ import { isDefined } from '@core/utils'; @@ -26,7 +26,7 @@ import { isDefined } from '@core/utils';
26 @Component({ 26 @Component({
27 selector: 'tb-boolean-filter-predicate', 27 selector: 'tb-boolean-filter-predicate',
28 templateUrl: './boolean-filter-predicate.component.html', 28 templateUrl: './boolean-filter-predicate.component.html',
29 - styleUrls: [], 29 + styleUrls: ['./filter-predicate.scss'],
30 providers: [ 30 providers: [
31 { 31 {
32 provide: NG_VALUE_ACCESSOR, 32 provide: NG_VALUE_ACCESSOR,
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<form [formGroup]="complexFilterFormGroup" (ngSubmit)="save()"> 18 +<form [formGroup]="complexFilterFormGroup" (ngSubmit)="save()" style="width: 700px;">
19 <mat-toolbar color="primary"> 19 <mat-toolbar color="primary">
20 <h2 translate>filter.complex-filter</h2> 20 <h2 translate>filter.complex-filter</h2>
21 <span fxFlex></span> 21 <span fxFlex></span>
@@ -38,6 +38,7 @@ @@ -38,6 +38,7 @@
38 <tb-filter-predicate-list 38 <tb-filter-predicate-list
39 [userMode]="data.userMode" 39 [userMode]="data.userMode"
40 [valueType]="data.valueType" 40 [valueType]="data.valueType"
  41 + [operation]="complexFilterFormGroup.get('operation').value"
41 formControlName="predicates"> 42 formControlName="predicates">
42 </tb-filter-predicate-list> 43 </tb-filter-predicate-list>
43 </fieldset> 44 </fieldset>
@@ -45,8 +46,8 @@ @@ -45,8 +46,8 @@
45 <div mat-dialog-actions fxLayoutAlign="end center"> 46 <div mat-dialog-actions fxLayoutAlign="end center">
46 <button mat-raised-button color="primary" 47 <button mat-raised-button color="primary"
47 type="submit" 48 type="submit"
48 - [disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid">  
49 - {{ 'action.update' | translate }} 49 + [disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty">
  50 + {{ (isAdd ? 'action.add' : 'action.update') | translate }}
50 </button> 51 </button>
51 <button mat-button color="primary" 52 <button mat-button color="primary"
52 type="button" 53 type="button"
@@ -33,6 +33,7 @@ export interface ComplexFilterPredicateDialogData { @@ -33,6 +33,7 @@ export interface ComplexFilterPredicateDialogData {
33 complexPredicate: ComplexFilterPredicate; 33 complexPredicate: ComplexFilterPredicate;
34 userMode: boolean; 34 userMode: boolean;
35 disabled: boolean; 35 disabled: boolean;
  36 + isAdd: boolean;
36 valueType: EntityKeyValueType; 37 valueType: EntityKeyValueType;
37 } 38 }
38 39
@@ -52,6 +53,8 @@ export class ComplexFilterPredicateDialogComponent extends @@ -52,6 +53,8 @@ export class ComplexFilterPredicateDialogComponent extends
52 complexOperationEnum = ComplexOperation; 53 complexOperationEnum = ComplexOperation;
53 complexOperationTranslations = complexOperationTranslationMap; 54 complexOperationTranslations = complexOperationTranslationMap;
54 55
  56 + isAdd: boolean;
  57 +
55 submitted = false; 58 submitted = false;
56 59
57 constructor(protected store: Store<AppState>, 60 constructor(protected store: Store<AppState>,
@@ -62,6 +65,8 @@ export class ComplexFilterPredicateDialogComponent extends @@ -62,6 +65,8 @@ export class ComplexFilterPredicateDialogComponent extends
62 private fb: FormBuilder) { 65 private fb: FormBuilder) {
63 super(store, router, dialogRef); 66 super(store, router, dialogRef);
64 67
  68 + this.isAdd = this.data.isAdd;
  69 +
65 this.complexFilterFormGroup = this.fb.group( 70 this.complexFilterFormGroup = this.fb.group(
66 { 71 {
67 operation: [this.data.complexPredicate.operation, [Validators.required]], 72 operation: [this.data.complexPredicate.operation, [Validators.required]],
@@ -15,9 +15,10 @@ @@ -15,9 +15,10 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div fxLayout="row"> 18 +<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
19 <mat-label translate>filter.complex-filter</mat-label> 19 <mat-label translate>filter.complex-filter</mat-label>
20 <button mat-icon-button color="primary" 20 <button mat-icon-button color="primary"
  21 + class="tb-mat-32"
21 [fxShow]="!disabled" 22 [fxShow]="!disabled"
22 type="button" 23 type="button"
23 (click)="openComplexFilterDialog()" 24 (click)="openComplexFilterDialog()"
@@ -78,7 +78,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On @@ -78,7 +78,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
78 complexPredicate: deepClone(this.complexFilterPredicate), 78 complexPredicate: deepClone(this.complexFilterPredicate),
79 disabled: this.disabled, 79 disabled: this.disabled,
80 userMode: this.userMode, 80 userMode: this.userMode,
81 - valueType: this.valueType 81 + valueType: this.valueType,
  82 + isAdd: false
82 } 83 }
83 }).afterClosed().subscribe( 84 }).afterClosed().subscribe(
84 (result) => { 85 (result) => {
@@ -15,9 +15,9 @@ @@ -15,9 +15,9 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="min-width: 480px;"> 18 +<form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="width: 700px;">
19 <mat-toolbar color="primary"> 19 <mat-toolbar color="primary">
20 - <h2>{{ (isAdd ? 'filter.add' : 'filter.edit') | translate }}</h2> 20 + <h2>{{ userMode ? filter.filter : ((isAdd ? 'filter.add' : 'filter.edit') | translate) }}</h2>
21 <span fxFlex></span> 21 <span fxFlex></span>
22 <button mat-icon-button 22 <button mat-icon-button
23 (click)="cancel()" 23 (click)="cancel()"
@@ -30,16 +30,24 @@ @@ -30,16 +30,24 @@
30 <div mat-dialog-content> 30 <div mat-dialog-content>
31 <fieldset [disabled]="isLoading$ | async"> 31 <fieldset [disabled]="isLoading$ | async">
32 <div fxFlex fxLayout="column"> 32 <div fxFlex fxLayout="column">
33 - <mat-form-field fxFlex class="mat-block">  
34 - <mat-label translate>filter.name</mat-label>  
35 - <input matInput formControlName="filter" required>  
36 - <mat-error *ngIf="filterFormGroup.get('filter').hasError('required')">  
37 - {{ 'filter.name-required' | translate }}  
38 - </mat-error>  
39 - <mat-error *ngIf="filterFormGroup.get('filter').hasError('duplicateFilterName')">  
40 - {{ 'filter.duplicate-filter' | translate }}  
41 - </mat-error>  
42 - </mat-form-field> 33 + <div fxLayout="row" [fxShow]="!userMode">
  34 + <mat-form-field fxFlex class="mat-block">
  35 + <mat-label translate>filter.name</mat-label>
  36 + <input matInput formControlName="filter" required>
  37 + <mat-error *ngIf="filterFormGroup.get('filter').hasError('required')">
  38 + {{ 'filter.name-required' | translate }}
  39 + </mat-error>
  40 + <mat-error *ngIf="filterFormGroup.get('filter').hasError('duplicateFilterName')">
  41 + {{ 'filter.duplicate-filter' | translate }}
  42 + </mat-error>
  43 + </mat-form-field>
  44 + <section class="tb-editable-switch" fxLayout="column" fxLayoutAlign="start center">
  45 + <label class="tb-small editable-label" translate>filter.editable</label>
  46 + <mat-slide-toggle class="editable-switch"
  47 + formControlName="editable">
  48 + </mat-slide-toggle>
  49 + </section>
  50 + </div>
43 <tb-key-filter-list 51 <tb-key-filter-list
44 formControlName="keyFilters" 52 formControlName="keyFilters"
45 [userMode]="userMode"> 53 [userMode]="userMode">
@@ -51,7 +59,7 @@ @@ -51,7 +59,7 @@
51 <button mat-raised-button color="primary" 59 <button mat-raised-button color="primary"
52 type="submit" 60 type="submit"
53 [disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty"> 61 [disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty">
54 - {{ (isAdd ? 'action.add' : 'action.update') | translate }} 62 + {{ (userMode ? 'action.update' : (isAdd ? 'action.add' : 'action.update')) | translate }}
55 </button> 63 </button>
56 <button mat-button color="primary" 64 <button mat-button color="primary"
57 type="button" 65 type="button"
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .tb-editable-switch {
  18 + padding-left: 10px;
  19 +
  20 + .editable-switch {
  21 + margin: 0;
  22 + }
  23 +
  24 + .editable-label {
  25 + margin: 5px 0;
  26 + }
  27 + }
  28 +}
@@ -45,7 +45,7 @@ export interface FilterDialogData { @@ -45,7 +45,7 @@ export interface FilterDialogData {
45 selector: 'tb-filter-dialog', 45 selector: 'tb-filter-dialog',
46 templateUrl: './filter-dialog.component.html', 46 templateUrl: './filter-dialog.component.html',
47 providers: [{provide: ErrorStateMatcher, useExisting: FilterDialogComponent}], 47 providers: [{provide: ErrorStateMatcher, useExisting: FilterDialogComponent}],
48 - styleUrls: [] 48 + styleUrls: ['./filter-dialog.component.scss']
49 }) 49 })
50 export class FilterDialogComponent extends DialogComponent<FilterDialogComponent, Filter> 50 export class FilterDialogComponent extends DialogComponent<FilterDialogComponent, Filter>
51 implements OnInit, ErrorStateMatcher { 51 implements OnInit, ErrorStateMatcher {
@@ -83,7 +83,8 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent @@ -83,7 +83,8 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
83 this.filter = { 83 this.filter = {
84 id: null, 84 id: null,
85 filter: '', 85 filter: '',
86 - keyFilters: [] 86 + keyFilters: [],
  87 + editable: true
87 }; 88 };
88 } else { 89 } else {
89 this.filter = data.filter; 90 this.filter = data.filter;
@@ -91,6 +92,7 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent @@ -91,6 +92,7 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
91 92
92 this.filterFormGroup = this.fb.group({ 93 this.filterFormGroup = this.fb.group({
93 filter: [this.filter.filter, [this.validateDuplicateFilterName(), Validators.required]], 94 filter: [this.filter.filter, [this.validateDuplicateFilterName(), Validators.required]],
  95 + editable: [this.filter.editable],
94 keyFilters: [this.filter.keyFilters, Validators.required] 96 keyFilters: [this.filter.keyFilters, Validators.required]
95 }); 97 });
96 } 98 }
@@ -128,6 +130,7 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent @@ -128,6 +130,7 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
128 save(): void { 130 save(): void {
129 this.submitted = true; 131 this.submitted = true;
130 this.filter.filter = this.filterFormGroup.get('filter').value; 132 this.filter.filter = this.filterFormGroup.get('filter').value;
  133 + this.filter.editable = this.filterFormGroup.get('editable').value;
131 this.filter.keyFilters = this.filterFormGroup.get('keyFilters').value; 134 this.filter.keyFilters = this.filterFormGroup.get('keyFilters').value;
132 if (this.isAdd) { 135 if (this.isAdd) {
133 this.filter.id = this.utils.guid(); 136 this.filter.id = this.utils.guid();
@@ -16,42 +16,72 @@ @@ -16,42 +16,72 @@
16 16
17 --> 17 -->
18 <section fxLayout="column" [formGroup]="filterListFormGroup"> 18 <section fxLayout="column" [formGroup]="filterListFormGroup">
19 - <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" style="max-height: 40px;"  
20 - formArrayName="predicates"  
21 - *ngFor="let predicateControl of predicatesFormArray().controls; let $index = index">  
22 - <tb-filter-predicate  
23 - [userMode]="userMode"  
24 - [valueType]="valueType"  
25 - [formControl]="predicateControl">  
26 - </tb-filter-predicate>  
27 - <button mat-icon-button color="primary"  
28 - [fxShow]="!disabled && !userMode"  
29 - type="button"  
30 - (click)="removePredicate($index)"  
31 - matTooltip="{{ 'filter.remove-filter' | translate }}"  
32 - matTooltipPosition="above">  
33 - <mat-icon>close</mat-icon>  
34 - </button>  
35 - </div>  
36 - <span [fxShow]="!predicatesFormArray().length"  
37 - fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"  
38 - class="no-data-found" translate>filter.no-filters</span>  
39 - <div style="margin-top: 8px;" fxLayout="row" fxLayoutGap="8px">  
40 - <button mat-button mat-raised-button color="primary"  
41 - [fxShow]="!disabled && !userMode"  
42 - (click)="addPredicate(false)"  
43 - type="button"  
44 - matTooltip="{{ 'filter.add-filter' | translate }}"  
45 - matTooltipPosition="above">  
46 - {{ 'action.add' | translate }}  
47 - </button>  
48 - <button mat-button mat-raised-button color="primary"  
49 - [fxShow]="!disabled && !userMode"  
50 - (click)="addPredicate(true)"  
51 - type="button"  
52 - matTooltip="{{ 'filter.add-complex-filter' | translate }}"  
53 - matTooltipPosition="above">  
54 - {{ 'filter.add-complex' | translate }}  
55 - </button>  
56 - </div> 19 + <mat-expansion-panel [expanded]="true">
  20 + <mat-expansion-panel-header>
  21 + <mat-panel-title>
  22 + <div translate>filter.filters</div>
  23 + </mat-panel-title>
  24 + </mat-expansion-panel-header>
  25 + <div fxLayout="row">
  26 + <span fxFlex="8"></span>
  27 + <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex="92">
  28 + <div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
  29 + <label fxFlex translate class="tb-title no-padding">filter.operation.operation</label>
  30 + <label *ngIf="valueType === valueTypeEnum.STRING"
  31 + translate class="tb-title no-padding" style="min-width: 70px;">filter.ignore-case</label>
  32 + </div>
  33 + <label fxFlex translate class="tb-title no-padding">filter.value</label>
  34 + <span [fxShow]="!disabled && !userMode" style="min-width: 40px;">&nbsp;</span>
  35 + </div>
  36 + </div>
  37 + <mat-divider></mat-divider>
  38 + <div class="predicate-list">
  39 + <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;"
  40 + formArrayName="predicates"
  41 + *ngFor="let predicateControl of predicatesFormArray().controls; let $index = index">
  42 + <div fxFlex="8" fxLayout="row" fxLayoutAlign="center" class="filters-operation">
  43 + <span *ngIf="$index > 0">{{ complexOperationTranslations.get(operation) | translate }}</span>
  44 + </div>
  45 + <div fxLayout="column" fxFlex="92">
  46 + <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex>
  47 + <tb-filter-predicate
  48 + fxFlex
  49 + [userMode]="userMode"
  50 + [valueType]="valueType"
  51 + [formControl]="predicateControl">
  52 + </tb-filter-predicate>
  53 + <button mat-icon-button color="primary"
  54 + [fxShow]="!disabled && !userMode"
  55 + type="button"
  56 + (click)="removePredicate($index)"
  57 + matTooltip="{{ 'filter.remove-filter' | translate }}"
  58 + matTooltipPosition="above">
  59 + <mat-icon>close</mat-icon>
  60 + </button>
  61 + </div>
  62 + </div>
  63 + </div>
  64 + <span [fxShow]="!predicatesFormArray().length"
  65 + fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
  66 + class="no-data-found" translate>filter.no-filters</span>
  67 + </div>
  68 + <div style="margin-top: 16px;" fxLayout="row" fxLayoutGap="8px">
  69 + <button mat-button mat-raised-button color="primary"
  70 + [fxShow]="!disabled && !userMode"
  71 + (click)="addPredicate(false)"
  72 + type="button"
  73 + matTooltip="{{ 'filter.add-filter' | translate }}"
  74 + matTooltipPosition="above">
  75 + {{ 'action.add' | translate }}
  76 + </button>
  77 + <button mat-button mat-raised-button color="primary"
  78 + [fxShow]="!disabled && !userMode"
  79 + (click)="addPredicate(true)"
  80 + type="button"
  81 + matTooltip="{{ 'filter.add-complex-filter' | translate }}"
  82 + matTooltipPosition="above">
  83 + {{ 'filter.add-complex' | translate }}
  84 + </button>
  85 + </div>
  86 + </mat-expansion-panel>
57 </section> 87 </section>
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .predicate-list {
  18 + overflow: auto;
  19 + max-height: 350px;
  20 + .no-data-found {
  21 + height: 50px;
  22 + }
  23 + }
  24 + .filters-operation {
  25 + margin-top: -40px;
  26 + color: #666;
  27 + font-weight: 500;
  28 + }
  29 +}
@@ -27,6 +27,7 @@ import { @@ -27,6 +27,7 @@ import {
27 import { Observable, of, Subscription } from 'rxjs'; 27 import { Observable, of, Subscription } from 'rxjs';
28 import { 28 import {
29 ComplexFilterPredicate, 29 ComplexFilterPredicate,
  30 + ComplexOperation, complexOperationTranslationMap,
30 createDefaultFilterPredicate, 31 createDefaultFilterPredicate,
31 EntityKeyValueType, 32 EntityKeyValueType,
32 KeyFilterPredicate 33 KeyFilterPredicate
@@ -40,7 +41,7 @@ import { MatDialog } from '@angular/material/dialog'; @@ -40,7 +41,7 @@ import { MatDialog } from '@angular/material/dialog';
40 @Component({ 41 @Component({
41 selector: 'tb-filter-predicate-list', 42 selector: 'tb-filter-predicate-list',
42 templateUrl: './filter-predicate-list.component.html', 43 templateUrl: './filter-predicate-list.component.html',
43 - styleUrls: [], 44 + styleUrls: ['./filter-predicate-list.component.scss'],
44 providers: [ 45 providers: [
45 { 46 {
46 provide: NG_VALUE_ACCESSOR, 47 provide: NG_VALUE_ACCESSOR,
@@ -57,8 +58,14 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni @@ -57,8 +58,14 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
57 58
58 @Input() valueType: EntityKeyValueType; 59 @Input() valueType: EntityKeyValueType;
59 60
  61 + @Input() operation: ComplexOperation = ComplexOperation.AND;
  62 +
60 filterListFormGroup: FormGroup; 63 filterListFormGroup: FormGroup;
61 64
  65 + valueTypeEnum = EntityKeyValueType;
  66 +
  67 + complexOperationTranslations = complexOperationTranslationMap;
  68 +
62 private propagateChange = null; 69 private propagateChange = null;
63 70
64 private valueChangeSubscription: Subscription = null; 71 private valueChangeSubscription: Subscription = null;
@@ -143,7 +150,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni @@ -143,7 +150,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
143 complexPredicate: predicate, 150 complexPredicate: predicate,
144 disabled: this.disabled, 151 disabled: this.disabled,
145 userMode: this.userMode, 152 userMode: this.userMode,
146 - valueType: this.valueType 153 + valueType: this.valueType,
  154 + isAdd: true
147 } 155 }
148 }).afterClosed(); 156 }).afterClosed();
149 } 157 }
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host ::ng-deep {
  17 + mat-form-field {
  18 + .mat-form-field-wrapper {
  19 + padding-bottom: 0;
  20 + .mat-form-field-infix {
  21 + border-top-width: 0.2em;
  22 + width: auto;
  23 + }
  24 + .mat-form-field-underline {
  25 + bottom: 0;
  26 + }
  27 + }
  28 + }
  29 +}
@@ -32,8 +32,11 @@ @@ -32,8 +32,11 @@
32 <div class="tb-filters-header" fxLayout="row" fxLayoutAlign="start center"> 32 <div class="tb-filters-header" fxLayout="row" fxLayoutAlign="start center">
33 <span fxFlex="5"></span> 33 <span fxFlex="5"></span>
34 <div fxFlex="95" fxLayout="row" fxLayoutAlign="start center"> 34 <div fxFlex="95" fxLayout="row" fxLayoutAlign="start center">
35 - <span class="tb-header-label" translate fxFlex>filter.filter</span>  
36 - <span style="min-width: 80px;"></span> 35 + <div class="tb-header-label" translate fxFlex>filter.filter</div>
  36 + <div class="tb-header-label" translate fxFlex="120px"
  37 + fxLayout="column" fxLayoutAlign="center center"
  38 + style="padding-left: 30px;">filter.editable</div>
  39 + <div style="min-width: 80px;"></div>
37 </div> 40 </div>
38 </div> 41 </div>
39 <fieldset [disabled]="isLoading$ | async"> 42 <fieldset [disabled]="isLoading$ | async">
@@ -43,13 +46,15 @@ @@ -43,13 +46,15 @@
43 *ngFor="let filterControl of filtersFormArray().controls; let $index = index"> 46 *ngFor="let filterControl of filtersFormArray().controls; let $index = index">
44 <span fxFlex="5">{{$index + 1}}.</span> 47 <span fxFlex="5">{{$index + 1}}.</span>
45 <div class="mat-elevation-z4 tb-filter" fxFlex="95" fxLayout="row" fxLayoutAlign="start center"> 48 <div class="mat-elevation-z4 tb-filter" fxFlex="95" fxLayout="row" fxLayoutAlign="start center">
46 - <mat-form-field floatLabel="always" hideRequiredMarker class="mat-block" fxFlex>  
47 - <mat-label></mat-label>  
48 - <input matInput [formControl]="filterControl.get('filter')" required placeholder="{{ 'filter.filter' | translate }}">  
49 - <mat-error *ngIf="filterControl.get('filter').hasError('required')">  
50 - {{ 'filter.filter-required' | translate }}  
51 - </mat-error>  
52 - </mat-form-field> 49 + <mat-label fxFlex>{{filterControl.get('filter').value}}</mat-label>
  50 + <section fxFlex="120px" style="padding-left: 10px;"
  51 + class="tb-editable-switch"
  52 + fxLayout="column"
  53 + fxLayoutAlign="center center">
  54 + <mat-slide-toggle class="editable-switch"
  55 + [formControl]="filterControl.get('editable')">
  56 + </mat-slide-toggle>
  57 + </section>
53 <button [disabled]="isLoading$ | async" 58 <button [disabled]="isLoading$ | async"
54 mat-icon-button color="primary" 59 mat-icon-button color="primary"
55 style="min-width: 40px;" 60 style="min-width: 40px;"
@@ -33,6 +33,14 @@ @@ -33,6 +33,14 @@
33 .tb-filter { 33 .tb-filter {
34 padding: 0 0 0 10px; 34 padding: 0 0 0 10px;
35 margin: 5px; 35 margin: 5px;
  36 +
  37 + .tb-editable-switch {
  38 + padding-left: 10px;
  39 +
  40 + .editable-switch {
  41 + margin: 0;
  42 + }
  43 + }
36 } 44 }
37 } 45 }
38 46
@@ -36,7 +36,7 @@ import { UtilsService } from '@core/services/utils.service'; @@ -36,7 +36,7 @@ import { UtilsService } from '@core/services/utils.service';
36 import { TranslateService } from '@ngx-translate/core'; 36 import { TranslateService } from '@ngx-translate/core';
37 import { ActionNotificationShow } from '@core/notification/notification.actions'; 37 import { ActionNotificationShow } from '@core/notification/notification.actions';
38 import { DialogService } from '@core/services/dialog.service'; 38 import { DialogService } from '@core/services/dialog.service';
39 -import { deepClone } from '@core/utils'; 39 +import { deepClone, isUndefined } from '@core/utils';
40 import { Filter, Filters, KeyFilterInfo } from '@shared/models/query/query.models'; 40 import { Filter, Filters, KeyFilterInfo } from '@shared/models/query/query.models';
41 import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; 41 import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
42 42
@@ -109,6 +109,9 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone @@ -109,6 +109,9 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
109 const filterControls: Array<AbstractControl> = []; 109 const filterControls: Array<AbstractControl> = [];
110 for (const filterId of Object.keys(this.data.filters)) { 110 for (const filterId of Object.keys(this.data.filters)) {
111 const filter = this.data.filters[filterId]; 111 const filter = this.data.filters[filterId];
  112 + if (isUndefined(filter.editable)) {
  113 + filter.editable = true;
  114 + }
112 filterControls.push(this.createFilterFormControl(filterId, filter)); 115 filterControls.push(this.createFilterFormControl(filterId, filter));
113 } 116 }
114 117
@@ -121,7 +124,8 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone @@ -121,7 +124,8 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
121 const filterFormControl = this.fb.group({ 124 const filterFormControl = this.fb.group({
122 id: [filterId], 125 id: [filterId],
123 filter: [filter ? filter.filter : null, [Validators.required]], 126 filter: [filter ? filter.filter : null, [Validators.required]],
124 - keyFilters: [filter ? filter.keyFilters : [], [Validators.required]] 127 + keyFilters: [filter ? filter.keyFilters : [], [Validators.required]],
  128 + editable: [filter ? filter.editable : true]
125 }); 129 });
126 return filterFormControl; 130 return filterFormControl;
127 } 131 }
@@ -148,9 +152,9 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone @@ -148,9 +152,9 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
148 for (const widgetTitle of widgetsTitleList) { 152 for (const widgetTitle of widgetsTitleList) {
149 widgetsListHtml += '<br/>\'' + widgetTitle + '\''; 153 widgetsListHtml += '<br/>\'' + widgetTitle + '\'';
150 } 154 }
151 - const message = this.translate.instant('entity.unable-delete-filter-text', 155 + const message = this.translate.instant('filter.unable-delete-filter-text',
152 {filter: filter.filter, widgetsList: widgetsListHtml}); 156 {filter: filter.filter, widgetsList: widgetsListHtml});
153 - this.dialogs.alert(this.translate.instant('entity.unable-delete-filter-title'), 157 + this.dialogs.alert(this.translate.instant('filter.unable-delete-filter-title'),
154 message, this.translate.instant('action.close'), true); 158 message, this.translate.instant('action.close'), true);
155 } else { 159 } else {
156 (this.filtersFormGroup.get('filters') as FormArray).removeAt(index); 160 (this.filtersFormGroup.get('filters') as FormArray).removeAt(index);
@@ -190,8 +194,9 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone @@ -190,8 +194,9 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
190 .push(this.createFilterFormControl(result.id, result)); 194 .push(this.createFilterFormControl(result.id, result));
191 } else { 195 } else {
192 const filterFormControl = (this.filtersFormGroup.get('filters') as FormArray).at(index); 196 const filterFormControl = (this.filtersFormGroup.get('filters') as FormArray).at(index);
193 - filterFormControl.get('filter').patchValue(filter.filter);  
194 - filterFormControl.get('keyFilters').patchValue(filter.keyFilters); 197 + filterFormControl.get('filter').patchValue(result.filter);
  198 + filterFormControl.get('editable').patchValue(result.editable);
  199 + filterFormControl.get('keyFilters').patchValue(result.keyFilters);
195 } 200 }
196 this.filtersFormGroup.markAsDirty(); 201 this.filtersFormGroup.markAsDirty();
197 } 202 }
@@ -215,6 +220,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone @@ -215,6 +220,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
215 const filterId: string = filterValue.id; 220 const filterId: string = filterValue.id;
216 const filter: string = filterValue.filter; 221 const filter: string = filterValue.filter;
217 const keyFilters: Array<KeyFilterInfo> = filterValue.keyFilters; 222 const keyFilters: Array<KeyFilterInfo> = filterValue.keyFilters;
  223 + const editable: boolean = filterValue.editable;
218 if (uniqueFilterList[filter]) { 224 if (uniqueFilterList[filter]) {
219 valid = false; 225 valid = false;
220 message = this.translate.instant('filter.duplicate-filter-error', {filter}); 226 message = this.translate.instant('filter.duplicate-filter-error', {filter});
@@ -225,7 +231,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone @@ -225,7 +231,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
225 break; 231 break;
226 } else { 232 } else {
227 uniqueFilterList[filter] = filter; 233 uniqueFilterList[filter] = filter;
228 - filters[filterId] = {id: filterId, filter, keyFilters}; 234 + filters[filterId] = {id: filterId, filter, keyFilters, editable};
229 } 235 }
230 } 236 }
231 if (valid) { 237 if (valid) {
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div fxLayout="column" class="mat-content mat-padding">
  19 + <div fxLayout="column" *ngFor="let filter of filtersInfo | keyvalue">
  20 + <div fxFlex fxLayout="row" fxLayoutAlign="start center">
  21 + <mat-label fxFlex>{{filter.value.filter}}</mat-label>
  22 + <button mat-icon-button color="primary"
  23 + style="min-width: 40px;"
  24 + type="button"
  25 + (click)="editFilter(filter.key, filter.value)"
  26 + matTooltip="{{ 'filter.edit' | translate }}"
  27 + matTooltipPosition="above">
  28 + <mat-icon>edit</mat-icon>
  29 + </button>
  30 + </div>
  31 + <mat-divider></mat-divider>
  32 + </div>
  33 +</div>
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + min-width: 300px;
  18 + max-height: 150px;
  19 + overflow-x: hidden;
  20 + overflow-y: auto;
  21 + background: #fff;
  22 + border-radius: 4px;
  23 + box-shadow:
  24 + 0 7px 8px -4px rgba(0, 0, 0, .2),
  25 + 0 13px 19px 2px rgba(0, 0, 0, .14),
  26 + 0 5px 24px 4px rgba(0, 0, 0, .12);
  27 +
  28 + @media (min-height: 350px) {
  29 + max-height: 250px;
  30 + }
  31 +
  32 + .mat-content {
  33 + background-color: #fff;
  34 + }
  35 +}
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, InjectionToken } from '@angular/core';
  18 +import { IAliasController } from '@core/api/widget-api.models';
  19 +import { Filter, FilterInfo } from '@shared/models/query/query.models';
  20 +import { MatDialog } from '@angular/material/dialog';
  21 +import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
  22 +import { deepClone } from '@core/utils';
  23 +
  24 +export const FILTER_EDIT_PANEL_DATA = new InjectionToken<any>('FiltersEditPanelData');
  25 +
  26 +export interface FiltersEditPanelData {
  27 + aliasController: IAliasController;
  28 + filtersInfo: {[filterId: string]: FilterInfo};
  29 +}
  30 +
  31 +@Component({
  32 + selector: 'tb-filters-edit-panel',
  33 + templateUrl: './filters-edit-panel.component.html',
  34 + styleUrls: ['./filters-edit-panel.component.scss']
  35 +})
  36 +export class FiltersEditPanelComponent {
  37 +
  38 + filtersInfo: {[filterId: string]: FilterInfo};
  39 +
  40 + constructor(@Inject(FILTER_EDIT_PANEL_DATA) public data: FiltersEditPanelData,
  41 + private dialog: MatDialog) {
  42 + this.filtersInfo = this.data.filtersInfo;
  43 + }
  44 +
  45 + public editFilter(filterId: string, filter: FilterInfo) {
  46 + const singleFilter: Filter = {id: filterId, ...deepClone(filter)};
  47 + this.dialog.open<FilterDialogComponent, FilterDialogData,
  48 + Filter>(FilterDialogComponent, {
  49 + disableClose: true,
  50 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  51 + data: {
  52 + isAdd: false,
  53 + filters: [],
  54 + filter: singleFilter,
  55 + userMode: true
  56 + }
  57 + }).afterClosed().subscribe(
  58 + (result) => {
  59 + if (result) {
  60 + this.filtersInfo[result.id] = result;
  61 + this.data.aliasController.updateUserFilter(result);
  62 + }
  63 + });
  64 + }
  65 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<section class="tb-filters-edit" fxLayout="row" fxLayoutAlign="start center" *ngIf="hasEditableFilters">
  19 + <button mat-icon-button
  20 + cdkOverlayOrigin #filtersEditPanelOrigin="cdkOverlayOrigin"
  21 + (click)="openEditMode()"
  22 + matTooltip="{{ 'filter.filters' | translate }}"
  23 + [matTooltipPosition]="tooltipPosition">
  24 + <mat-icon>filter_list</mat-icon>
  25 + </button>
  26 + <span fxHide.lt-lg
  27 + (click)="openEditMode()"
  28 + matTooltip="{{ 'filter.filters' | translate }}"
  29 + [matTooltipPosition]="tooltipPosition">
  30 + {{ 'filter.filters' | translate }}
  31 + </span>
  32 +</section>
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import "../../../../../scss/constants";
  17 +
  18 +:host {
  19 + section.tb-filters-edit {
  20 + min-height: 32px;
  21 + padding: 0 6px;
  22 +
  23 + @media #{$mat-lt-md} {
  24 + padding: 0;
  25 + }
  26 +
  27 + span {
  28 + max-width: 200px;
  29 + overflow: hidden;
  30 + text-overflow: ellipsis;
  31 + white-space: nowrap;
  32 + pointer-events: all;
  33 + cursor: pointer;
  34 + }
  35 + }
  36 +}
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
  18 +import { TooltipPosition } from '@angular/material/tooltip';
  19 +import { IAliasController } from '@core/api/widget-api.models';
  20 +import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
  21 +import { TranslateService } from '@ngx-translate/core';
  22 +import { Subscription } from 'rxjs';
  23 +import { BreakpointObserver } from '@angular/cdk/layout';
  24 +import { deepClone } from '@core/utils';
  25 +import { FilterInfo } from '@shared/models/query/query.models';
  26 +import {
  27 + FILTER_EDIT_PANEL_DATA,
  28 + FiltersEditPanelComponent,
  29 + FiltersEditPanelData
  30 +} from '@home/components/filter/filters-edit-panel.component';
  31 +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  32 +
  33 +@Component({
  34 + selector: 'tb-filters-edit',
  35 + templateUrl: './filters-edit.component.html',
  36 + styleUrls: ['./filters-edit.component.scss']
  37 +})
  38 +export class FiltersEditComponent implements OnInit, OnDestroy {
  39 +
  40 + aliasControllerValue: IAliasController;
  41 +
  42 + @Input()
  43 + set aliasController(aliasController: IAliasController) {
  44 + this.aliasControllerValue = aliasController;
  45 + this.setupAliasController(this.aliasControllerValue);
  46 + }
  47 +
  48 + get aliasController(): IAliasController {
  49 + return this.aliasControllerValue;
  50 + }
  51 +
  52 + @Input()
  53 + tooltipPosition: TooltipPosition = 'above';
  54 +
  55 + @Input() disabled: boolean;
  56 +
  57 + @ViewChild('filtersEditPanelOrigin') filtersEditPanelOrigin: CdkOverlayOrigin;
  58 +
  59 + displayValue: string;
  60 + filtersInfo: {[filterId: string]: FilterInfo} = {};
  61 + hasEditableFilters = false;
  62 +
  63 + private rxSubscriptions = new Array<Subscription>();
  64 +
  65 + constructor(private translate: TranslateService,
  66 + private overlay: Overlay,
  67 + private breakpointObserver: BreakpointObserver,
  68 + private viewContainerRef: ViewContainerRef) {
  69 + }
  70 +
  71 + private setupAliasController(aliasController: IAliasController) {
  72 + this.rxSubscriptions.forEach((subscription) => {
  73 + subscription.unsubscribe();
  74 + });
  75 + this.rxSubscriptions.length = 0;
  76 + if (aliasController) {
  77 + this.rxSubscriptions.push(aliasController.filtersChanged.subscribe(
  78 + () => {
  79 + setTimeout(() => {
  80 + this.updateFiltersInfo();
  81 + }, 0);
  82 + }
  83 + ));
  84 + setTimeout(() => {
  85 + this.updateFiltersInfo();
  86 + }, 0);
  87 + }
  88 + }
  89 +
  90 + ngOnInit(): void {
  91 + }
  92 +
  93 + ngOnDestroy(): void {
  94 + this.rxSubscriptions.forEach((subscription) => {
  95 + subscription.unsubscribe();
  96 + });
  97 + this.rxSubscriptions.length = 0;
  98 + }
  99 +
  100 + openEditMode() {
  101 + if (this.disabled || !this.hasEditableFilters) {
  102 + return;
  103 + }
  104 + const position = this.overlay.position();
  105 + const config = new OverlayConfig({
  106 + panelClass: 'tb-filters-edit-panel',
  107 + backdropClass: 'cdk-overlay-transparent-backdrop',
  108 + hasBackdrop: true,
  109 + });
  110 + const connectedPosition: ConnectedPosition = {
  111 + originX: 'start',
  112 + originY: 'bottom',
  113 + overlayX: 'start',
  114 + overlayY: 'top'
  115 + };
  116 + config.positionStrategy = position.flexibleConnectedTo(this.filtersEditPanelOrigin.elementRef)
  117 + .withPositions([connectedPosition]);
  118 + const overlayRef = this.overlay.create(config);
  119 + overlayRef.backdropClick().subscribe(() => {
  120 + overlayRef.dispose();
  121 + });
  122 +
  123 + const injector = this._createFiltersEditPanelInjector(
  124 + overlayRef,
  125 + {
  126 + aliasController: this.aliasController,
  127 + filtersInfo: deepClone(this.filtersInfo)
  128 + }
  129 + );
  130 + overlayRef.attach(new ComponentPortal(FiltersEditPanelComponent, this.viewContainerRef, injector));
  131 + }
  132 +
  133 + private _createFiltersEditPanelInjector(overlayRef: OverlayRef, data: FiltersEditPanelData): PortalInjector {
  134 + const injectionTokens = new WeakMap<any, any>([
  135 + [FILTER_EDIT_PANEL_DATA, data],
  136 + [OverlayRef, overlayRef]
  137 + ]);
  138 + return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  139 + }
  140 +
  141 + private updateFiltersInfo() {
  142 + const allFilters = this.aliasController.getFilters();
  143 + this.filtersInfo = {};
  144 + this.hasEditableFilters = false;
  145 + for (const filterId of Object.keys(allFilters)) {
  146 + const filterInfo = this.aliasController.getFilterInfo(filterId);
  147 + if (filterInfo && filterInfo.editable) {
  148 + this.filtersInfo[filterId] = deepClone(filterInfo);
  149 + this.hasEditableFilters = true;
  150 + }
  151 + }
  152 + }
  153 +
  154 +}
@@ -15,9 +15,9 @@ @@ -15,9 +15,9 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<form [formGroup]="keyFilterFormGroup" (ngSubmit)="save()"> 18 +<form [formGroup]="keyFilterFormGroup" (ngSubmit)="save()" style="width: 700px;">
19 <mat-toolbar color="primary"> 19 <mat-toolbar color="primary">
20 - <h2 translate>{{data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter'}}</h2> 20 + <h2>{{data.userMode ? data.keyFilter.key.key : ((data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter') | translate)}}</h2>
21 <span fxFlex></span> 21 <span fxFlex></span>
22 <button mat-icon-button 22 <button mat-icon-button
23 (click)="cancel()" 23 (click)="cancel()"
@@ -27,16 +27,16 @@ @@ -27,16 +27,16 @@
27 </mat-toolbar> 27 </mat-toolbar>
28 <div mat-dialog-content> 28 <div mat-dialog-content>
29 <fieldset [disabled]="isLoading$ | async" fxLayout="column"> 29 <fieldset [disabled]="isLoading$ | async" fxLayout="column">
30 - <section fxLayout="row" fxLayoutGap="8px">  
31 - <section fxFlex="60" fxLayout="row" formGroupName="key" fxLayoutGap="8px">  
32 - <mat-form-field fxFlex="40" class="mat-block"> 30 + <section fxLayout="row" fxLayoutGap="8px" class="entity-key" [fxShow]="!data.userMode">
  31 + <section fxFlex="70" fxLayout="row" formGroupName="key" fxLayoutGap="8px">
  32 + <mat-form-field fxFlex="60" class="mat-block">
33 <mat-label translate>filter.key-name</mat-label> 33 <mat-label translate>filter.key-name</mat-label>
34 <input matInput required formControlName="key"> 34 <input matInput required formControlName="key">
35 <mat-error *ngIf="keyFilterFormGroup.get('key.key').hasError('required')"> 35 <mat-error *ngIf="keyFilterFormGroup.get('key.key').hasError('required')">
36 {{ 'filter.key-name-required' | translate }} 36 {{ 'filter.key-name-required' | translate }}
37 </mat-error> 37 </mat-error>
38 </mat-form-field> 38 </mat-form-field>
39 - <mat-form-field fxFlex="60" class="mat-block"> 39 + <mat-form-field fxFlex="40" class="mat-block">
40 <mat-label translate>filter.key-type.key-type</mat-label> 40 <mat-label translate>filter.key-type.key-type</mat-label>
41 <mat-select required formControlName="type"> 41 <mat-select required formControlName="type">
42 <mat-option *ngFor="let type of entityKeyTypes" [value]="type"> 42 <mat-option *ngFor="let type of entityKeyTypes" [value]="type">
@@ -45,15 +45,15 @@ @@ -45,15 +45,15 @@
45 </mat-select> 45 </mat-select>
46 </mat-form-field> 46 </mat-form-field>
47 </section> 47 </section>
48 - <mat-form-field fxFlex="40" class="mat-block"> 48 + <mat-form-field fxFlex="30" class="mat-block">
49 <mat-label translate>filter.value-type.value-type</mat-label> 49 <mat-label translate>filter.value-type.value-type</mat-label>
50 <mat-select matInput formControlName="valueType"> 50 <mat-select matInput formControlName="valueType">
51 <mat-select-trigger> 51 <mat-select-trigger>
52 - <mat-icon svgIcon="{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.icon }}"></mat-icon> 52 + <mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.icon }}"></mat-icon>
53 <span>{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }}</span> 53 <span>{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }}</span>
54 </mat-select-trigger> 54 </mat-select-trigger>
55 <mat-option *ngFor="let valueType of entityKeyValueTypesKeys" [value]="valueType"> 55 <mat-option *ngFor="let valueType of entityKeyValueTypesKeys" [value]="valueType">
56 - <mat-icon svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon> 56 + <mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon>
57 <span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span> 57 <span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span>
58 </mat-option> 58 </mat-option>
59 </mat-select> 59 </mat-select>
@@ -72,7 +72,7 @@ @@ -72,7 +72,7 @@
72 <div mat-dialog-actions fxLayoutAlign="end center"> 72 <div mat-dialog-actions fxLayoutAlign="end center">
73 <button mat-raised-button color="primary" 73 <button mat-raised-button color="primary"
74 type="submit" 74 type="submit"
75 - [disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid"> 75 + [disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid || !keyFilterFormGroup.dirty">
76 {{ (data.isAdd ? 'action.add' : 'action.update') | translate }} 76 {{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
77 </button> 77 </button>
78 <button mat-button color="primary" 78 <button mat-button color="primary"
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host ::ng-deep {
  17 + .entity-key {
  18 + mat-form-field {
  19 + .mat-form-field-wrapper {
  20 + .mat-form-field-infix {
  21 + width: auto;
  22 + }
  23 + }
  24 + }
  25 + }
  26 +}
@@ -27,8 +27,10 @@ import { @@ -27,8 +27,10 @@ import {
27 entityKeyTypeTranslationMap, 27 entityKeyTypeTranslationMap,
28 EntityKeyValueType, 28 EntityKeyValueType,
29 entityKeyValueTypesMap, 29 entityKeyValueTypesMap,
30 - KeyFilterInfo 30 + KeyFilterInfo, KeyFilterPredicate
31 } from '@shared/models/query/query.models'; 31 } from '@shared/models/query/query.models';
  32 +import { DialogService } from '@core/services/dialog.service';
  33 +import { TranslateService } from '@ngx-translate/core';
32 34
33 export interface KeyFilterDialogData { 35 export interface KeyFilterDialogData {
34 keyFilter: KeyFilterInfo; 36 keyFilter: KeyFilterInfo;
@@ -40,7 +42,7 @@ export interface KeyFilterDialogData { @@ -40,7 +42,7 @@ export interface KeyFilterDialogData {
40 selector: 'tb-key-filter-dialog', 42 selector: 'tb-key-filter-dialog',
41 templateUrl: './key-filter-dialog.component.html', 43 templateUrl: './key-filter-dialog.component.html',
42 providers: [{provide: ErrorStateMatcher, useExisting: KeyFilterDialogComponent}], 44 providers: [{provide: ErrorStateMatcher, useExisting: KeyFilterDialogComponent}],
43 - styleUrls: [] 45 + styleUrls: ['./key-filter-dialog.component.scss']
44 }) 46 })
45 export class KeyFilterDialogComponent extends 47 export class KeyFilterDialogComponent extends
46 DialogComponent<KeyFilterDialogComponent, KeyFilterInfo> 48 DialogComponent<KeyFilterDialogComponent, KeyFilterInfo>
@@ -65,6 +67,8 @@ export class KeyFilterDialogComponent extends @@ -65,6 +67,8 @@ export class KeyFilterDialogComponent extends
65 @Inject(MAT_DIALOG_DATA) public data: KeyFilterDialogData, 67 @Inject(MAT_DIALOG_DATA) public data: KeyFilterDialogData,
66 @SkipSelf() private errorStateMatcher: ErrorStateMatcher, 68 @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
67 public dialogRef: MatDialogRef<KeyFilterDialogComponent, KeyFilterInfo>, 69 public dialogRef: MatDialogRef<KeyFilterDialogComponent, KeyFilterInfo>,
  70 + private dialogs: DialogService,
  71 + private translate: TranslateService,
68 private fb: FormBuilder) { 72 private fb: FormBuilder) {
69 super(store, router, dialogRef); 73 super(store, router, dialogRef);
70 74
@@ -80,6 +84,22 @@ export class KeyFilterDialogComponent extends @@ -80,6 +84,22 @@ export class KeyFilterDialogComponent extends
80 predicates: [this.data.keyFilter.predicates, [Validators.required]] 84 predicates: [this.data.keyFilter.predicates, [Validators.required]]
81 } 85 }
82 ); 86 );
  87 + this.keyFilterFormGroup.get('valueType').valueChanges.subscribe((valueType: EntityKeyValueType) => {
  88 + const prevValue: EntityKeyValueType = this.keyFilterFormGroup.value.valueType;
  89 + const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value;
  90 + if (prevValue && prevValue !== valueType && predicates && predicates.length) {
  91 + this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'),
  92 + this.translate.instant('filter.key-value-type-change-message')).subscribe(
  93 + (result) => {
  94 + if (result) {
  95 + this.keyFilterFormGroup.get('predicates').setValue([]);
  96 + } else {
  97 + this.keyFilterFormGroup.get('valueType').setValue(prevValue, {emitEvent: false});
  98 + }
  99 + }
  100 + );
  101 + }
  102 + });
83 } 103 }
84 104
85 ngOnInit(): void { 105 ngOnInit(): void {
@@ -16,38 +16,65 @@ @@ -16,38 +16,65 @@
16 16
17 --> 17 -->
18 <section fxLayout="column" [formGroup]="keyFilterListFormGroup"> 18 <section fxLayout="column" [formGroup]="keyFilterListFormGroup">
19 - <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" style="max-height: 40px;"  
20 - formArrayName="keyFilters"  
21 - *ngFor="let keyFilterControl of keyFiltersFormArray().controls; let $index = index">  
22 - <span>{{ keyFilterControl.value.key.key }}</span>  
23 - <span>{{ keyFilterControl.value.key.type }}</span>  
24 - <button mat-icon-button color="primary"  
25 - type="button"  
26 - (click)="editKeyFilter($index)"  
27 - matTooltip="{{ 'filter.edit-key-filter' | translate }}"  
28 - matTooltipPosition="above">  
29 - <mat-icon>edit</mat-icon>  
30 - </button>  
31 - <button mat-icon-button color="primary"  
32 - [fxShow]="!disabled && !userMode"  
33 - type="button"  
34 - (click)="removeKeyFilter($index)"  
35 - matTooltip="{{ 'filter.remove-key-filter' | translate }}"  
36 - matTooltipPosition="above">  
37 - <mat-icon>close</mat-icon>  
38 - </button>  
39 - </div>  
40 - <span [fxShow]="!keyFiltersFormArray().length"  
41 - fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"  
42 - class="no-data-found" translate>filter.no-key-filters</span>  
43 - <div style="margin-top: 8px;">  
44 - <button mat-button mat-raised-button color="primary"  
45 - [fxShow]="!disabled && !userMode"  
46 - (click)="addKeyFilter()"  
47 - type="button"  
48 - matTooltip="{{ 'filter.add-key-filter' | translate }}"  
49 - matTooltipPosition="above">  
50 - {{ 'filter.add-key-filter' | translate }}  
51 - </button>  
52 - </div> 19 + <mat-expansion-panel [expanded]="true">
  20 + <mat-expansion-panel-header>
  21 + <mat-panel-title>
  22 + <div translate>filter.key-filters</div>
  23 + </mat-panel-title>
  24 + </mat-expansion-panel-header>
  25 + <div fxLayout="row">
  26 + <span fxFlex="8"></span>
  27 + <div fxLayout="row" fxLayoutAlign="start center" fxFlex="92">
  28 + <label fxFlex translate class="tb-title no-padding">filter.key-name</label>
  29 + <label fxFlex translate class="tb-title no-padding">filter.key-type.key-type</label>
  30 + <span [fxShow]="!disabled && !userMode" style="min-width: 80px;">&nbsp;</span>
  31 + <span [fxShow]="disabled || userMode" style="min-width: 40px;">&nbsp;</span>
  32 + </div>
  33 + </div>
  34 + <mat-divider></mat-divider>
  35 + <div class="key-filter-list">
  36 + <div fxLayout="row" fxLayoutAlign="start center" style="max-height: 40px;"
  37 + formArrayName="keyFilters"
  38 + *ngFor="let keyFilterControl of keyFiltersFormArray().controls; let $index = index">
  39 + <div fxFlex="8" class="filters-operation">
  40 + <span *ngIf="$index > 0" translate>filter.operation.and</span>
  41 + </div>
  42 + <div fxLayout="column" fxFlex="92">
  43 + <div fxLayout="row" fxLayoutAlign="start center" fxFlex>
  44 + <div fxFlex>{{ keyFilterControl.value.key.key }}</div>
  45 + <div fxFlex translate>{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}</div>
  46 + <button mat-icon-button color="primary"
  47 + type="button"
  48 + (click)="editKeyFilter($index)"
  49 + matTooltip="{{ 'filter.edit-key-filter' | translate }}"
  50 + matTooltipPosition="above">
  51 + <mat-icon>edit</mat-icon>
  52 + </button>
  53 + <button mat-icon-button color="primary"
  54 + [fxShow]="!disabled && !userMode"
  55 + type="button"
  56 + (click)="removeKeyFilter($index)"
  57 + matTooltip="{{ 'filter.remove-key-filter' | translate }}"
  58 + matTooltipPosition="above">
  59 + <mat-icon>close</mat-icon>
  60 + </button>
  61 + </div>
  62 + <mat-divider></mat-divider>
  63 + </div>
  64 + </div>
  65 + <span [fxShow]="!keyFiltersFormArray().length"
  66 + fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
  67 + class="no-data-found" translate>filter.no-key-filters</span>
  68 + </div>
  69 + <div style="margin-top: 16px;">
  70 + <button mat-button mat-raised-button color="primary"
  71 + [fxShow]="!disabled && !userMode"
  72 + (click)="addKeyFilter()"
  73 + type="button"
  74 + matTooltip="{{ 'filter.add-key-filter' | translate }}"
  75 + matTooltipPosition="above">
  76 + {{ 'filter.add-key-filter' | translate }}
  77 + </button>
  78 + </div>
  79 + </mat-expansion-panel>
53 </section> 80 </section>
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .key-filter-list {
  18 + overflow: auto;
  19 + max-height: 300px;
  20 + .no-data-found {
  21 + height: 50px;
  22 + }
  23 + }
  24 + .filters-operation {
  25 + margin-top: -40px;
  26 + color: #666;
  27 + font-weight: 500;
  28 + }
  29 +}
@@ -25,7 +25,7 @@ import { @@ -25,7 +25,7 @@ import {
25 Validators 25 Validators
26 } from '@angular/forms'; 26 } from '@angular/forms';
27 import { Observable, Subscription } from 'rxjs'; 27 import { Observable, Subscription } from 'rxjs';
28 -import { EntityKeyType, KeyFilterInfo } from '@shared/models/query/query.models'; 28 +import { EntityKeyType, entityKeyTypeTranslationMap, KeyFilterInfo } from '@shared/models/query/query.models';
29 import { MatDialog } from '@angular/material/dialog'; 29 import { MatDialog } from '@angular/material/dialog';
30 import { deepClone } from '@core/utils'; 30 import { deepClone } from '@core/utils';
31 import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component'; 31 import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component';
@@ -33,7 +33,7 @@ import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/ @@ -33,7 +33,7 @@ import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/
33 @Component({ 33 @Component({
34 selector: 'tb-key-filter-list', 34 selector: 'tb-key-filter-list',
35 templateUrl: './key-filter-list.component.html', 35 templateUrl: './key-filter-list.component.html',
36 - styleUrls: [], 36 + styleUrls: ['./key-filter-list.component.scss'],
37 providers: [ 37 providers: [
38 { 38 {
39 provide: NG_VALUE_ACCESSOR, 39 provide: NG_VALUE_ACCESSOR,
@@ -50,6 +50,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { @@ -50,6 +50,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
50 50
51 keyFilterListFormGroup: FormGroup; 51 keyFilterListFormGroup: FormGroup;
52 52
  53 + entityKeyTypeTranslations = entityKeyTypeTranslationMap;
  54 +
53 private propagateChange = null; 55 private propagateChange = null;
54 56
55 private valueChangeSubscription: Subscription = null; 57 private valueChangeSubscription: Subscription = null;
@@ -15,17 +15,18 @@ @@ -15,17 +15,18 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div fxLayout="row" [formGroup]="numericFilterPredicateFormGroup">  
19 - <mat-form-field class="mat-block">  
20 - <mat-label translate>filter.operation.operation</mat-label>  
21 - <mat-select required formControlName="operation"> 18 +<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="numericFilterPredicateFormGroup">
  19 + <mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
  20 + <mat-label></mat-label>
  21 + <mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
22 <mat-option *ngFor="let operation of numericOperations" [value]="operation"> 22 <mat-option *ngFor="let operation of numericOperations" [value]="operation">
23 {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} 23 {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}}
24 </mat-option> 24 </mat-option>
25 </mat-select> 25 </mat-select>
26 </mat-form-field> 26 </mat-form-field>
27 - <mat-form-field class="mat-block">  
28 - <mat-label translate>filter.value</mat-label>  
29 - <input required type="number" matInput formControlName="value"> 27 + <mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
  28 + <mat-label></mat-label>
  29 + <input required type="number" matInput formControlName="value"
  30 + placeholder="{{'filter.value' | translate}}">
30 </mat-form-field> 31 </mat-form-field>
31 </div> 32 </div>
@@ -24,7 +24,7 @@ import { isDefined } from '@core/utils'; @@ -24,7 +24,7 @@ import { isDefined } from '@core/utils';
24 @Component({ 24 @Component({
25 selector: 'tb-numeric-filter-predicate', 25 selector: 'tb-numeric-filter-predicate',
26 templateUrl: './numeric-filter-predicate.component.html', 26 templateUrl: './numeric-filter-predicate.component.html',
27 - styleUrls: [], 27 + styleUrls: ['./filter-predicate.scss'],
28 providers: [ 28 providers: [
29 { 29 {
30 provide: NG_VALUE_ACCESSOR, 30 provide: NG_VALUE_ACCESSOR,
@@ -15,20 +15,21 @@ @@ -15,20 +15,21 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div fxLayout="row" [formGroup]="stringFilterPredicateFormGroup">  
19 - <mat-form-field class="mat-block">  
20 - <mat-label translate>filter.operation.operation</mat-label>  
21 - <mat-select required formControlName="operation">  
22 - <mat-option *ngFor="let operation of stringOperations" [value]="operation">  
23 - {{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}}  
24 - </mat-option>  
25 - </mat-select>  
26 - </mat-form-field>  
27 - <mat-checkbox formControlName="ignoreCase">  
28 - {{ 'filter.ignore-case' | translate }}  
29 - </mat-checkbox>  
30 - <mat-form-field class="mat-block">  
31 - <mat-label translate>filter.value</mat-label>  
32 - <input matInput formControlName="value"> 18 +<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="stringFilterPredicateFormGroup">
  19 + <div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
  20 + <mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
  21 + <mat-label></mat-label>
  22 + <mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
  23 + <mat-option *ngFor="let operation of stringOperations" [value]="operation">
  24 + {{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}}
  25 + </mat-option>
  26 + </mat-select>
  27 + </mat-form-field>
  28 + <mat-checkbox fxLayout="row" fxLayoutAlign="center" formControlName="ignoreCase" style="min-width: 70px;">
  29 + </mat-checkbox>
  30 + </div>
  31 + <mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
  32 + <mat-label></mat-label>
  33 + <input matInput formControlName="value" placeholder="{{'filter.value' | translate}}">
33 </mat-form-field> 34 </mat-form-field>
34 </div> 35 </div>
@@ -26,7 +26,7 @@ import { @@ -26,7 +26,7 @@ import {
26 @Component({ 26 @Component({
27 selector: 'tb-string-filter-predicate', 27 selector: 'tb-string-filter-predicate',
28 templateUrl: './string-filter-predicate.component.html', 28 templateUrl: './string-filter-predicate.component.html',
29 - styleUrls: [], 29 + styleUrls: ['./filter-predicate.scss'],
30 providers: [ 30 providers: [
31 { 31 {
32 provide: NG_VALUE_ACCESSOR, 32 provide: NG_VALUE_ACCESSOR,
@@ -78,6 +78,8 @@ import { KeyFilterDialogComponent } from '@home/components/filter/key-filter-dia @@ -78,6 +78,8 @@ import { KeyFilterDialogComponent } from '@home/components/filter/key-filter-dia
78 import { FiltersDialogComponent } from '@home/components/filter/filters-dialog.component'; 78 import { FiltersDialogComponent } from '@home/components/filter/filters-dialog.component';
79 import { FilterDialogComponent } from '@home/components/filter/filter-dialog.component'; 79 import { FilterDialogComponent } from '@home/components/filter/filter-dialog.component';
80 import { FilterSelectComponent } from './filter/filter-select.component'; 80 import { FilterSelectComponent } from './filter/filter-select.component';
  81 +import { FiltersEditComponent } from '@home/components/filter/filters-edit.component';
  82 +import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.component';
81 83
82 @NgModule({ 84 @NgModule({
83 declarations: 85 declarations:
@@ -138,7 +140,9 @@ import { FilterSelectComponent } from './filter/filter-select.component'; @@ -138,7 +140,9 @@ import { FilterSelectComponent } from './filter/filter-select.component';
138 KeyFilterDialogComponent, 140 KeyFilterDialogComponent,
139 FilterDialogComponent, 141 FilterDialogComponent,
140 FiltersDialogComponent, 142 FiltersDialogComponent,
141 - FilterSelectComponent 143 + FilterSelectComponent,
  144 + FiltersEditComponent,
  145 + FiltersEditPanelComponent
142 ], 146 ],
143 imports: [ 147 imports: [
144 CommonModule, 148 CommonModule,
@@ -192,7 +196,8 @@ import { FilterSelectComponent } from './filter/filter-select.component'; @@ -192,7 +196,8 @@ import { FilterSelectComponent } from './filter/filter-select.component';
192 KeyFilterDialogComponent, 196 KeyFilterDialogComponent,
193 FilterDialogComponent, 197 FilterDialogComponent,
194 FiltersDialogComponent, 198 FiltersDialogComponent,
195 - FilterSelectComponent 199 + FilterSelectComponent,
  200 + FiltersEditComponent
196 ], 201 ],
197 providers: [ 202 providers: [
198 WidgetComponentService, 203 WidgetComponentService,
@@ -721,7 +721,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -721,7 +721,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
721 } 721 }
722 722
723 private createFilter(filter: string): Observable<Filter> { 723 private createFilter(filter: string): Observable<Filter> {
724 - const singleFilter: Filter = {id: null, filter, keyFilters: []}; 724 + const singleFilter: Filter = {id: null, filter, keyFilters: [], editable: true};
725 return this.dialog.open<FilterDialogComponent, FilterDialogData, 725 return this.dialog.open<FilterDialogComponent, FilterDialogData,
726 Filter>(FilterDialogComponent, { 726 Filter>(FilterDialogComponent, {
727 disableClose: true, 727 disableClose: true,
@@ -91,6 +91,10 @@ @@ -91,6 +91,10 @@
91 aggregation="true" 91 aggregation="true"
92 [(ngModel)]="dashboardCtx.dashboardTimewindow"> 92 [(ngModel)]="dashboardCtx.dashboardTimewindow">
93 </tb-timewindow> 93 </tb-timewindow>
  94 + <tb-filters-edit [fxShow]="!isEdit && displayFilters()"
  95 + tooltipPosition="below"
  96 + [aliasController]="dashboardCtx.aliasController">
  97 + </tb-filters-edit>
94 <tb-aliases-entity-select [fxShow]="!isEdit && displayEntitiesSelect()" 98 <tb-aliases-entity-select [fxShow]="!isEdit && displayEntitiesSelect()"
95 tooltipPosition="below" 99 tooltipPosition="below"
96 [aliasController]="dashboardCtx.aliasController"> 100 [aliasController]="dashboardCtx.aliasController">
@@ -410,6 +410,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -410,6 +410,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
410 } 410 }
411 } 411 }
412 412
  413 + public displayFilters(): boolean {
  414 + if (this.dashboard.configuration.settings &&
  415 + isDefined(this.dashboard.configuration.settings.showFilters)) {
  416 + return this.dashboard.configuration.settings.showFilters;
  417 + } else {
  418 + return true;
  419 + }
  420 + }
  421 +
413 public showRightLayoutSwitch(): boolean { 422 public showRightLayoutSwitch(): boolean {
414 return this.isMobile && this.layouts.right.show; 423 return this.isMobile && this.layouts.right.show;
415 } 424 }
@@ -59,6 +59,9 @@ @@ -59,6 +59,9 @@
59 <mat-checkbox fxFlex formControlName="showEntitiesSelect"> 59 <mat-checkbox fxFlex formControlName="showEntitiesSelect">
60 {{ 'dashboard.display-entities-selection' | translate }} 60 {{ 'dashboard.display-entities-selection' | translate }}
61 </mat-checkbox> 61 </mat-checkbox>
  62 + <mat-checkbox fxFlex formControlName="showFilters">
  63 + {{ 'dashboard.display-filters' | translate }}
  64 + </mat-checkbox>
62 <mat-checkbox fxFlex formControlName="showDashboardTimewindow"> 65 <mat-checkbox fxFlex formControlName="showDashboardTimewindow">
63 {{ 'dashboard.display-dashboard-timewindow' | translate }} 66 {{ 'dashboard.display-dashboard-timewindow' | translate }}
64 </mat-checkbox> 67 </mat-checkbox>
@@ -78,6 +78,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS @@ -78,6 +78,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
78 titleColor: [isUndefined(this.settings.titleColor) ? 'rgba(0,0,0,0.870588)' : this.settings.titleColor, []], 78 titleColor: [isUndefined(this.settings.titleColor) ? 'rgba(0,0,0,0.870588)' : this.settings.titleColor, []],
79 showDashboardsSelect: [isUndefined(this.settings.showDashboardsSelect) ? true : this.settings.showDashboardsSelect, []], 79 showDashboardsSelect: [isUndefined(this.settings.showDashboardsSelect) ? true : this.settings.showDashboardsSelect, []],
80 showEntitiesSelect: [isUndefined(this.settings.showEntitiesSelect) ? true : this.settings.showEntitiesSelect, []], 80 showEntitiesSelect: [isUndefined(this.settings.showEntitiesSelect) ? true : this.settings.showEntitiesSelect, []],
  81 + showFilters: [isUndefined(this.settings.showFilters) ? true : this.settings.showFilters, []],
81 showDashboardTimewindow: [isUndefined(this.settings.showDashboardTimewindow) ? true : this.settings.showDashboardTimewindow, []], 82 showDashboardTimewindow: [isUndefined(this.settings.showDashboardTimewindow) ? true : this.settings.showDashboardTimewindow, []],
82 showDashboardExport: [isUndefined(this.settings.showDashboardExport) ? true : this.settings.showDashboardExport, []] 83 showDashboardExport: [isUndefined(this.settings.showDashboardExport) ? true : this.settings.showDashboardExport, []]
83 }); 84 });
@@ -85,6 +85,7 @@ export interface DashboardSettings { @@ -85,6 +85,7 @@ export interface DashboardSettings {
85 showTitle?: boolean; 85 showTitle?: boolean;
86 showDashboardsSelect?: boolean; 86 showDashboardsSelect?: boolean;
87 showEntitiesSelect?: boolean; 87 showEntitiesSelect?: boolean;
  88 + showFilters?: boolean;
88 showDashboardTimewindow?: boolean; 89 showDashboardTimewindow?: boolean;
89 showDashboardExport?: boolean; 90 showDashboardExport?: boolean;
90 toolbarAlwaysOpen?: boolean; 91 toolbarAlwaysOpen?: boolean;
@@ -248,6 +248,7 @@ export interface KeyFilterInfo { @@ -248,6 +248,7 @@ export interface KeyFilterInfo {
248 248
249 export interface FilterInfo { 249 export interface FilterInfo {
250 filter: string; 250 filter: string;
  251 + editable: boolean;
251 keyFilters: Array<KeyFilterInfo>; 252 keyFilters: Array<KeyFilterInfo>;
252 } 253 }
253 254
@@ -549,6 +549,7 @@ @@ -549,6 +549,7 @@
549 "title-color": "Title color", 549 "title-color": "Title color",
550 "display-dashboards-selection": "Display dashboards selection", 550 "display-dashboards-selection": "Display dashboards selection",
551 "display-entities-selection": "Display entities selection", 551 "display-entities-selection": "Display entities selection",
  552 + "display-filters": "Display filters",
552 "display-dashboard-timewindow": "Display timewindow", 553 "display-dashboard-timewindow": "Display timewindow",
553 "display-dashboard-export": "Display export", 554 "display-dashboard-export": "Display export",
554 "import": "Import dashboard", 555 "import": "Import dashboard",
@@ -1166,6 +1167,7 @@ @@ -1166,6 +1167,7 @@
1166 "duplicate-filter-error": "Duplicate filter found '{{filter}}'.<br>Filters must be unique within the dashboard.", 1167 "duplicate-filter-error": "Duplicate filter found '{{filter}}'.<br>Filters must be unique within the dashboard.",
1167 "missing-key-filters-error": "Key filters is missing for filter '{{filter}}'.", 1168 "missing-key-filters-error": "Key filters is missing for filter '{{filter}}'.",
1168 "filter": "Filter", 1169 "filter": "Filter",
  1170 + "editable": "Editable",
1169 "no-filters-found": "No filters found.", 1171 "no-filters-found": "No filters found.",
1170 "no-filter-matching": "'{{filter}}' not found.", 1172 "no-filter-matching": "'{{filter}}' not found.",
1171 "create-new-filter": "Create a new one!", 1173 "create-new-filter": "Create a new one!",
@@ -1195,6 +1197,7 @@ @@ -1195,6 +1197,7 @@
1195 "complex-filter": "Complex filter", 1197 "complex-filter": "Complex filter",
1196 "edit-complex-filter": "Edit complex filter", 1198 "edit-complex-filter": "Edit complex filter",
1197 "key-filter": "Key filter", 1199 "key-filter": "Key filter",
  1200 + "key-filters": "Key filters",
1198 "key-name": "Key name", 1201 "key-name": "Key name",
1199 "key-name-required": "Key name is required.", 1202 "key-name-required": "Key name is required.",
1200 "key-type": { 1203 "key-type": {
@@ -1210,6 +1213,8 @@ @@ -1210,6 +1213,8 @@
1210 "boolean": "Boolean" 1213 "boolean": "Boolean"
1211 }, 1214 },
1212 "value-type-required": "Key value type is required.", 1215 "value-type-required": "Key value type is required.",
  1216 + "key-value-type-change-title": "Are you sure you want to change key value type?",
  1217 + "key-value-type-change-message": "If you confirm new value type all entered key filters will be removed.",
1213 "no-key-filters": "No key filters configured", 1218 "no-key-filters": "No key filters configured",
1214 "add-key-filter": "Add key filter", 1219 "add-key-filter": "Add key filter",
1215 "remove-key-filter": "Remove key filter", 1220 "remove-key-filter": "Remove key filter",
@@ -750,6 +750,9 @@ mat-label { @@ -750,6 +750,9 @@ mat-label {
750 &.tb-mat-16 { 750 &.tb-mat-16 {
751 @include tb-mat-icon-size(16); 751 @include tb-mat-icon-size(16);
752 } 752 }
  753 + &.tb-mat-18 {
  754 + @include tb-mat-icon-size(18);
  755 + }
753 &.tb-mat-20 { 756 &.tb-mat-20 {
754 @include tb-mat-icon-size(20); 757 @include tb-mat-icon-size(20);
755 } 758 }