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 24 import { EntityInfo } from '@shared/models/entity.models';
25 25 import { map, mergeMap } from 'rxjs/operators';
26 26 import {
27   - defaultEntityDataPageLink, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink,
  27 + defaultEntityDataPageLink, Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink,
28 28 updateDatasourceFromEntityInfo
29 29 } from '@shared/models/query/query.models';
30 30
... ... @@ -41,6 +41,7 @@ export class AliasController implements IAliasController {
41 41
42 42 entityAliases: EntityAliases;
43 43 filters: Filters;
  44 + userFilters: Filters;
44 45
45 46 resolvedAliases: {[aliasId: string]: AliasInfo} = {};
46 47 resolvedAliasesObservable: {[aliasId: string]: Observable<AliasInfo>} = {};
... ... @@ -54,6 +55,7 @@ export class AliasController implements IAliasController {
54 55 private origFilters: Filters) {
55 56 this.entityAliases = deepClone(this.origEntityAliases);
56 57 this.filters = deepClone(this.origFilters);
  58 + this.userFilters = {};
57 59 }
58 60
59 61 updateEntityAliases(newEntityAliases: EntityAliases) {
... ... @@ -94,6 +96,9 @@ export class AliasController implements IAliasController {
94 96 }
95 97 this.filters = deepClone(newFilters);
96 98 if (changedFilterIds.length) {
  99 + for (const filterId of changedFilterIds) {
  100 + delete this.userFilters[filterId];
  101 + }
97 102 this.filtersChangedSubject.next(changedFilterIds);
98 103 }
99 104 }
... ... @@ -146,7 +151,11 @@ export class AliasController implements IAliasController {
146 151 }
147 152
148 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 161 getKeyFilters(filterId: string): Array<KeyFilter> {
... ... @@ -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 68 isPaginatedDataSubscription?: boolean;
69 69 pageLink?: EntityDataPageLink;
70 70 keyFilters?: Array<KeyFilter>;
  71 + additionalKeyFilters?: Array<KeyFilter>;
71 72 subscriptionTimewindow?: SubscriptionTimewindow;
72 73 }
73 74
... ... @@ -206,10 +207,19 @@ export class EntityDataSubscription {
206 207 this.subscriber = new TelemetrySubscriber(this.telemetryService);
207 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 219 this.dataCommand.query = {
210 220 entityFilter: this.entityDataSubscriptionOptions.entityFilter,
211 221 pageLink: this.entityDataSubscriptionOptions.pageLink,
212   - keyFilters: this.entityDataSubscriptionOptions.keyFilters,
  222 + keyFilters,
213 223 entityFields,
214 224 latestValues: this.latestValues
215 225 };
... ...
... ... @@ -65,7 +65,7 @@ export class EntityDataService {
65 65 return of(null);
66 66 }
67 67 listener.subscription = this.createSubscription(listener,
68   - datasource.pageLink, datasource.keyFilters,
  68 + datasource.pageLink, datasource.keyFilters, null,
69 69 false);
70 70 return listener.subscription.subscribe();
71 71 }
... ... @@ -86,15 +86,8 @@ export class EntityDataService {
86 86 if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !pageLink)) {
87 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 89 listener.subscription = this.createSubscription(listener,
97   - pageLink, keyFilters, true);
  90 + pageLink, datasource.keyFilters, keyFilters,true);
98 91 if (listener.subscriptionType === widgetType.timeseries) {
99 92 listener.subscription.entityDataSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
100 93 }
... ... @@ -110,6 +103,7 @@ export class EntityDataService {
110 103 private createSubscription(listener: EntityDataListener,
111 104 pageLink: EntityDataPageLink,
112 105 keyFilters: KeyFilter[],
  106 + additionalKeyFilters: KeyFilter[],
113 107 isPaginatedDataSubscription: boolean): EntityDataSubscription {
114 108 const datasource = listener.configDatasource;
115 109 const subscriptionDataKeys: Array<SubscriptionDataKey> = [];
... ... @@ -131,6 +125,7 @@ export class EntityDataService {
131 125 entityDataSubscriptionOptions.entityFilter = datasource.entityFilter;
132 126 entityDataSubscriptionOptions.pageLink = pageLink;
133 127 entityDataSubscriptionOptions.keyFilters = keyFilters;
  128 + entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters;
134 129 }
135 130 entityDataSubscriptionOptions.isPaginatedDataSubscription = isPaginatedDataSubscription;
136 131 return new EntityDataSubscription(entityDataSubscriptionOptions,
... ...
... ... @@ -112,6 +112,7 @@ export interface IAliasController {
112 112 getFilterInfo(filterId: string): FilterInfo;
113 113 getKeyFilters(filterId: string): Array<KeyFilter>;
114 114 updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo);
  115 + updateUserFilter(filter: Filter);
115 116 updateEntityAliases(entityAliases: EntityAliases);
116 117 updateFilters(filters: Filters);
117 118 updateAliases(aliasIds?: Array<string>);
... ...
... ... @@ -1011,7 +1011,7 @@ export class WidgetSubscription implements IWidgetSubscription {
1011 1011 const entityDataListener = this.entityDataListeners[datasourceIndex];
1012 1012 if (entityDataListener) {
1013 1013 const pageLink = entityDataListener.subscription.entityDataSubscriptionOptions.pageLink;
1014   - const keyFilters = entityDataListener.subscription.entityDataSubscriptionOptions.keyFilters;
  1014 + const keyFilters = entityDataListener.subscription.entityDataSubscriptionOptions.additionalKeyFilters;
1015 1015 this.subscribeForPaginatedData(datasourceIndex, pageLink, keyFilters);
1016 1016 }
1017 1017 }
... ...
... ... @@ -410,7 +410,8 @@ export class ItemBufferService {
410 410 private prepareFilterInfo(filter: Filter): FilterInfo {
411 411 return {
412 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 514 if (!newFilterId) {
514 515 const newFilterName = this.createFilterName(filters, filterInfo.filter);
515 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 520 return newFilterId;
519 521 }
... ...
... ... @@ -15,16 +15,16 @@
15 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 22 <mat-option *ngFor="let operation of booleanOperations" [value]="operation">
23 23 {{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}}
24 24 </mat-option>
25 25 </mat-select>
26 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 29 </mat-checkbox>
30 30 </div>
... ...
... ... @@ -26,7 +26,7 @@ import { isDefined } from '@core/utils';
26 26 @Component({
27 27 selector: 'tb-boolean-filter-predicate',
28 28 templateUrl: './boolean-filter-predicate.component.html',
29   - styleUrls: [],
  29 + styleUrls: ['./filter-predicate.scss'],
30 30 providers: [
31 31 {
32 32 provide: NG_VALUE_ACCESSOR,
... ...
... ... @@ -15,7 +15,7 @@
15 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 19 <mat-toolbar color="primary">
20 20 <h2 translate>filter.complex-filter</h2>
21 21 <span fxFlex></span>
... ... @@ -38,6 +38,7 @@
38 38 <tb-filter-predicate-list
39 39 [userMode]="data.userMode"
40 40 [valueType]="data.valueType"
  41 + [operation]="complexFilterFormGroup.get('operation').value"
41 42 formControlName="predicates">
42 43 </tb-filter-predicate-list>
43 44 </fieldset>
... ... @@ -45,8 +46,8 @@
45 46 <div mat-dialog-actions fxLayoutAlign="end center">
46 47 <button mat-raised-button color="primary"
47 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 51 </button>
51 52 <button mat-button color="primary"
52 53 type="button"
... ...
... ... @@ -33,6 +33,7 @@ export interface ComplexFilterPredicateDialogData {
33 33 complexPredicate: ComplexFilterPredicate;
34 34 userMode: boolean;
35 35 disabled: boolean;
  36 + isAdd: boolean;
36 37 valueType: EntityKeyValueType;
37 38 }
38 39
... ... @@ -52,6 +53,8 @@ export class ComplexFilterPredicateDialogComponent extends
52 53 complexOperationEnum = ComplexOperation;
53 54 complexOperationTranslations = complexOperationTranslationMap;
54 55
  56 + isAdd: boolean;
  57 +
55 58 submitted = false;
56 59
57 60 constructor(protected store: Store<AppState>,
... ... @@ -62,6 +65,8 @@ export class ComplexFilterPredicateDialogComponent extends
62 65 private fb: FormBuilder) {
63 66 super(store, router, dialogRef);
64 67
  68 + this.isAdd = this.data.isAdd;
  69 +
65 70 this.complexFilterFormGroup = this.fb.group(
66 71 {
67 72 operation: [this.data.complexPredicate.operation, [Validators.required]],
... ...
... ... @@ -15,9 +15,10 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<div fxLayout="row">
  18 +<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
19 19 <mat-label translate>filter.complex-filter</mat-label>
20 20 <button mat-icon-button color="primary"
  21 + class="tb-mat-32"
21 22 [fxShow]="!disabled"
22 23 type="button"
23 24 (click)="openComplexFilterDialog()"
... ...
... ... @@ -78,7 +78,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
78 78 complexPredicate: deepClone(this.complexFilterPredicate),
79 79 disabled: this.disabled,
80 80 userMode: this.userMode,
81   - valueType: this.valueType
  81 + valueType: this.valueType,
  82 + isAdd: false
82 83 }
83 84 }).afterClosed().subscribe(
84 85 (result) => {
... ...
... ... @@ -15,9 +15,9 @@
15 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 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 21 <span fxFlex></span>
22 22 <button mat-icon-button
23 23 (click)="cancel()"
... ... @@ -30,16 +30,24 @@
30 30 <div mat-dialog-content>
31 31 <fieldset [disabled]="isLoading$ | async">
32 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 51 <tb-key-filter-list
44 52 formControlName="keyFilters"
45 53 [userMode]="userMode">
... ... @@ -51,7 +59,7 @@
51 59 <button mat-raised-button color="primary"
52 60 type="submit"
53 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 63 </button>
56 64 <button mat-button color="primary"
57 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 45 selector: 'tb-filter-dialog',
46 46 templateUrl: './filter-dialog.component.html',
47 47 providers: [{provide: ErrorStateMatcher, useExisting: FilterDialogComponent}],
48   - styleUrls: []
  48 + styleUrls: ['./filter-dialog.component.scss']
49 49 })
50 50 export class FilterDialogComponent extends DialogComponent<FilterDialogComponent, Filter>
51 51 implements OnInit, ErrorStateMatcher {
... ... @@ -83,7 +83,8 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
83 83 this.filter = {
84 84 id: null,
85 85 filter: '',
86   - keyFilters: []
  86 + keyFilters: [],
  87 + editable: true
87 88 };
88 89 } else {
89 90 this.filter = data.filter;
... ... @@ -91,6 +92,7 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
91 92
92 93 this.filterFormGroup = this.fb.group({
93 94 filter: [this.filter.filter, [this.validateDuplicateFilterName(), Validators.required]],
  95 + editable: [this.filter.editable],
94 96 keyFilters: [this.filter.keyFilters, Validators.required]
95 97 });
96 98 }
... ... @@ -128,6 +130,7 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
128 130 save(): void {
129 131 this.submitted = true;
130 132 this.filter.filter = this.filterFormGroup.get('filter').value;
  133 + this.filter.editable = this.filterFormGroup.get('editable').value;
131 134 this.filter.keyFilters = this.filterFormGroup.get('keyFilters').value;
132 135 if (this.isAdd) {
133 136 this.filter.id = this.utils.guid();
... ...
... ... @@ -16,42 +16,72 @@
16 16
17 17 -->
18 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 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 27 import { Observable, of, Subscription } from 'rxjs';
28 28 import {
29 29 ComplexFilterPredicate,
  30 + ComplexOperation, complexOperationTranslationMap,
30 31 createDefaultFilterPredicate,
31 32 EntityKeyValueType,
32 33 KeyFilterPredicate
... ... @@ -40,7 +41,7 @@ import { MatDialog } from '@angular/material/dialog';
40 41 @Component({
41 42 selector: 'tb-filter-predicate-list',
42 43 templateUrl: './filter-predicate-list.component.html',
43   - styleUrls: [],
  44 + styleUrls: ['./filter-predicate-list.component.scss'],
44 45 providers: [
45 46 {
46 47 provide: NG_VALUE_ACCESSOR,
... ... @@ -57,8 +58,14 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
57 58
58 59 @Input() valueType: EntityKeyValueType;
59 60
  61 + @Input() operation: ComplexOperation = ComplexOperation.AND;
  62 +
60 63 filterListFormGroup: FormGroup;
61 64
  65 + valueTypeEnum = EntityKeyValueType;
  66 +
  67 + complexOperationTranslations = complexOperationTranslationMap;
  68 +
62 69 private propagateChange = null;
63 70
64 71 private valueChangeSubscription: Subscription = null;
... ... @@ -143,7 +150,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
143 150 complexPredicate: predicate,
144 151 disabled: this.disabled,
145 152 userMode: this.userMode,
146   - valueType: this.valueType
  153 + valueType: this.valueType,
  154 + isAdd: true
147 155 }
148 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 32 <div class="tb-filters-header" fxLayout="row" fxLayoutAlign="start center">
33 33 <span fxFlex="5"></span>
34 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 40 </div>
38 41 </div>
39 42 <fieldset [disabled]="isLoading$ | async">
... ... @@ -43,13 +46,15 @@
43 46 *ngFor="let filterControl of filtersFormArray().controls; let $index = index">
44 47 <span fxFlex="5">{{$index + 1}}.</span>
45 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 58 <button [disabled]="isLoading$ | async"
54 59 mat-icon-button color="primary"
55 60 style="min-width: 40px;"
... ...
... ... @@ -33,6 +33,14 @@
33 33 .tb-filter {
34 34 padding: 0 0 0 10px;
35 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 36 import { TranslateService } from '@ngx-translate/core';
37 37 import { ActionNotificationShow } from '@core/notification/notification.actions';
38 38 import { DialogService } from '@core/services/dialog.service';
39   -import { deepClone } from '@core/utils';
  39 +import { deepClone, isUndefined } from '@core/utils';
40 40 import { Filter, Filters, KeyFilterInfo } from '@shared/models/query/query.models';
41 41 import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
42 42
... ... @@ -109,6 +109,9 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
109 109 const filterControls: Array<AbstractControl> = [];
110 110 for (const filterId of Object.keys(this.data.filters)) {
111 111 const filter = this.data.filters[filterId];
  112 + if (isUndefined(filter.editable)) {
  113 + filter.editable = true;
  114 + }
112 115 filterControls.push(this.createFilterFormControl(filterId, filter));
113 116 }
114 117
... ... @@ -121,7 +124,8 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
121 124 const filterFormControl = this.fb.group({
122 125 id: [filterId],
123 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 130 return filterFormControl;
127 131 }
... ... @@ -148,9 +152,9 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
148 152 for (const widgetTitle of widgetsTitleList) {
149 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 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 158 message, this.translate.instant('action.close'), true);
155 159 } else {
156 160 (this.filtersFormGroup.get('filters') as FormArray).removeAt(index);
... ... @@ -190,8 +194,9 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
190 194 .push(this.createFilterFormControl(result.id, result));
191 195 } else {
192 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 201 this.filtersFormGroup.markAsDirty();
197 202 }
... ... @@ -215,6 +220,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
215 220 const filterId: string = filterValue.id;
216 221 const filter: string = filterValue.filter;
217 222 const keyFilters: Array<KeyFilterInfo> = filterValue.keyFilters;
  223 + const editable: boolean = filterValue.editable;
218 224 if (uniqueFilterList[filter]) {
219 225 valid = false;
220 226 message = this.translate.instant('filter.duplicate-filter-error', {filter});
... ... @@ -225,7 +231,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
225 231 break;
226 232 } else {
227 233 uniqueFilterList[filter] = filter;
228   - filters[filterId] = {id: filterId, filter, keyFilters};
  234 + filters[filterId] = {id: filterId, filter, keyFilters, editable};
229 235 }
230 236 }
231 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 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 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 21 <span fxFlex></span>
22 22 <button mat-icon-button
23 23 (click)="cancel()"
... ... @@ -27,16 +27,16 @@
27 27 </mat-toolbar>
28 28 <div mat-dialog-content>
29 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 33 <mat-label translate>filter.key-name</mat-label>
34 34 <input matInput required formControlName="key">
35 35 <mat-error *ngIf="keyFilterFormGroup.get('key.key').hasError('required')">
36 36 {{ 'filter.key-name-required' | translate }}
37 37 </mat-error>
38 38 </mat-form-field>
39   - <mat-form-field fxFlex="60" class="mat-block">
  39 + <mat-form-field fxFlex="40" class="mat-block">
40 40 <mat-label translate>filter.key-type.key-type</mat-label>
41 41 <mat-select required formControlName="type">
42 42 <mat-option *ngFor="let type of entityKeyTypes" [value]="type">
... ... @@ -45,15 +45,15 @@
45 45 </mat-select>
46 46 </mat-form-field>
47 47 </section>
48   - <mat-form-field fxFlex="40" class="mat-block">
  48 + <mat-form-field fxFlex="30" class="mat-block">
49 49 <mat-label translate>filter.value-type.value-type</mat-label>
50 50 <mat-select matInput formControlName="valueType">
51 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 53 <span>{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }}</span>
54 54 </mat-select-trigger>
55 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 57 <span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span>
58 58 </mat-option>
59 59 </mat-select>
... ... @@ -72,7 +72,7 @@
72 72 <div mat-dialog-actions fxLayoutAlign="end center">
73 73 <button mat-raised-button color="primary"
74 74 type="submit"
75   - [disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid">
  75 + [disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid || !keyFilterFormGroup.dirty">
76 76 {{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
77 77 </button>
78 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 27 entityKeyTypeTranslationMap,
28 28 EntityKeyValueType,
29 29 entityKeyValueTypesMap,
30   - KeyFilterInfo
  30 + KeyFilterInfo, KeyFilterPredicate
31 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 35 export interface KeyFilterDialogData {
34 36 keyFilter: KeyFilterInfo;
... ... @@ -40,7 +42,7 @@ export interface KeyFilterDialogData {
40 42 selector: 'tb-key-filter-dialog',
41 43 templateUrl: './key-filter-dialog.component.html',
42 44 providers: [{provide: ErrorStateMatcher, useExisting: KeyFilterDialogComponent}],
43   - styleUrls: []
  45 + styleUrls: ['./key-filter-dialog.component.scss']
44 46 })
45 47 export class KeyFilterDialogComponent extends
46 48 DialogComponent<KeyFilterDialogComponent, KeyFilterInfo>
... ... @@ -65,6 +67,8 @@ export class KeyFilterDialogComponent extends
65 67 @Inject(MAT_DIALOG_DATA) public data: KeyFilterDialogData,
66 68 @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
67 69 public dialogRef: MatDialogRef<KeyFilterDialogComponent, KeyFilterInfo>,
  70 + private dialogs: DialogService,
  71 + private translate: TranslateService,
68 72 private fb: FormBuilder) {
69 73 super(store, router, dialogRef);
70 74
... ... @@ -80,6 +84,22 @@ export class KeyFilterDialogComponent extends
80 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 105 ngOnInit(): void {
... ...
... ... @@ -16,38 +16,65 @@
16 16
17 17 -->
18 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 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 25 Validators
26 26 } from '@angular/forms';
27 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 29 import { MatDialog } from '@angular/material/dialog';
30 30 import { deepClone } from '@core/utils';
31 31 import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component';
... ... @@ -33,7 +33,7 @@ import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/
33 33 @Component({
34 34 selector: 'tb-key-filter-list',
35 35 templateUrl: './key-filter-list.component.html',
36   - styleUrls: [],
  36 + styleUrls: ['./key-filter-list.component.scss'],
37 37 providers: [
38 38 {
39 39 provide: NG_VALUE_ACCESSOR,
... ... @@ -50,6 +50,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
50 50
51 51 keyFilterListFormGroup: FormGroup;
52 52
  53 + entityKeyTypeTranslations = entityKeyTypeTranslationMap;
  54 +
53 55 private propagateChange = null;
54 56
55 57 private valueChangeSubscription: Subscription = null;
... ...
... ... @@ -15,17 +15,18 @@
15 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 22 <mat-option *ngFor="let operation of numericOperations" [value]="operation">
23 23 {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}}
24 24 </mat-option>
25 25 </mat-select>
26 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 31 </mat-form-field>
31 32 </div>
... ...
... ... @@ -24,7 +24,7 @@ import { isDefined } from '@core/utils';
24 24 @Component({
25 25 selector: 'tb-numeric-filter-predicate',
26 26 templateUrl: './numeric-filter-predicate.component.html',
27   - styleUrls: [],
  27 + styleUrls: ['./filter-predicate.scss'],
28 28 providers: [
29 29 {
30 30 provide: NG_VALUE_ACCESSOR,
... ...
... ... @@ -15,20 +15,21 @@
15 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 34 </mat-form-field>
34 35 </div>
... ...
... ... @@ -26,7 +26,7 @@ import {
26 26 @Component({
27 27 selector: 'tb-string-filter-predicate',
28 28 templateUrl: './string-filter-predicate.component.html',
29   - styleUrls: [],
  29 + styleUrls: ['./filter-predicate.scss'],
30 30 providers: [
31 31 {
32 32 provide: NG_VALUE_ACCESSOR,
... ...
... ... @@ -78,6 +78,8 @@ import { KeyFilterDialogComponent } from '@home/components/filter/key-filter-dia
78 78 import { FiltersDialogComponent } from '@home/components/filter/filters-dialog.component';
79 79 import { FilterDialogComponent } from '@home/components/filter/filter-dialog.component';
80 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 84 @NgModule({
83 85 declarations:
... ... @@ -138,7 +140,9 @@ import { FilterSelectComponent } from './filter/filter-select.component';
138 140 KeyFilterDialogComponent,
139 141 FilterDialogComponent,
140 142 FiltersDialogComponent,
141   - FilterSelectComponent
  143 + FilterSelectComponent,
  144 + FiltersEditComponent,
  145 + FiltersEditPanelComponent
142 146 ],
143 147 imports: [
144 148 CommonModule,
... ... @@ -192,7 +196,8 @@ import { FilterSelectComponent } from './filter/filter-select.component';
192 196 KeyFilterDialogComponent,
193 197 FilterDialogComponent,
194 198 FiltersDialogComponent,
195   - FilterSelectComponent
  199 + FilterSelectComponent,
  200 + FiltersEditComponent
196 201 ],
197 202 providers: [
198 203 WidgetComponentService,
... ...
... ... @@ -721,7 +721,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
721 721 }
722 722
723 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 725 return this.dialog.open<FilterDialogComponent, FilterDialogData,
726 726 Filter>(FilterDialogComponent, {
727 727 disableClose: true,
... ...
... ... @@ -91,6 +91,10 @@
91 91 aggregation="true"
92 92 [(ngModel)]="dashboardCtx.dashboardTimewindow">
93 93 </tb-timewindow>
  94 + <tb-filters-edit [fxShow]="!isEdit && displayFilters()"
  95 + tooltipPosition="below"
  96 + [aliasController]="dashboardCtx.aliasController">
  97 + </tb-filters-edit>
94 98 <tb-aliases-entity-select [fxShow]="!isEdit && displayEntitiesSelect()"
95 99 tooltipPosition="below"
96 100 [aliasController]="dashboardCtx.aliasController">
... ...
... ... @@ -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 422 public showRightLayoutSwitch(): boolean {
414 423 return this.isMobile && this.layouts.right.show;
415 424 }
... ...
... ... @@ -59,6 +59,9 @@
59 59 <mat-checkbox fxFlex formControlName="showEntitiesSelect">
60 60 {{ 'dashboard.display-entities-selection' | translate }}
61 61 </mat-checkbox>
  62 + <mat-checkbox fxFlex formControlName="showFilters">
  63 + {{ 'dashboard.display-filters' | translate }}
  64 + </mat-checkbox>
62 65 <mat-checkbox fxFlex formControlName="showDashboardTimewindow">
63 66 {{ 'dashboard.display-dashboard-timewindow' | translate }}
64 67 </mat-checkbox>
... ...
... ... @@ -78,6 +78,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
78 78 titleColor: [isUndefined(this.settings.titleColor) ? 'rgba(0,0,0,0.870588)' : this.settings.titleColor, []],
79 79 showDashboardsSelect: [isUndefined(this.settings.showDashboardsSelect) ? true : this.settings.showDashboardsSelect, []],
80 80 showEntitiesSelect: [isUndefined(this.settings.showEntitiesSelect) ? true : this.settings.showEntitiesSelect, []],
  81 + showFilters: [isUndefined(this.settings.showFilters) ? true : this.settings.showFilters, []],
81 82 showDashboardTimewindow: [isUndefined(this.settings.showDashboardTimewindow) ? true : this.settings.showDashboardTimewindow, []],
82 83 showDashboardExport: [isUndefined(this.settings.showDashboardExport) ? true : this.settings.showDashboardExport, []]
83 84 });
... ...
... ... @@ -85,6 +85,7 @@ export interface DashboardSettings {
85 85 showTitle?: boolean;
86 86 showDashboardsSelect?: boolean;
87 87 showEntitiesSelect?: boolean;
  88 + showFilters?: boolean;
88 89 showDashboardTimewindow?: boolean;
89 90 showDashboardExport?: boolean;
90 91 toolbarAlwaysOpen?: boolean;
... ...
... ... @@ -248,6 +248,7 @@ export interface KeyFilterInfo {
248 248
249 249 export interface FilterInfo {
250 250 filter: string;
  251 + editable: boolean;
251 252 keyFilters: Array<KeyFilterInfo>;
252 253 }
253 254
... ...
... ... @@ -549,6 +549,7 @@
549 549 "title-color": "Title color",
550 550 "display-dashboards-selection": "Display dashboards selection",
551 551 "display-entities-selection": "Display entities selection",
  552 + "display-filters": "Display filters",
552 553 "display-dashboard-timewindow": "Display timewindow",
553 554 "display-dashboard-export": "Display export",
554 555 "import": "Import dashboard",
... ... @@ -1166,6 +1167,7 @@
1166 1167 "duplicate-filter-error": "Duplicate filter found '{{filter}}'.<br>Filters must be unique within the dashboard.",
1167 1168 "missing-key-filters-error": "Key filters is missing for filter '{{filter}}'.",
1168 1169 "filter": "Filter",
  1170 + "editable": "Editable",
1169 1171 "no-filters-found": "No filters found.",
1170 1172 "no-filter-matching": "'{{filter}}' not found.",
1171 1173 "create-new-filter": "Create a new one!",
... ... @@ -1195,6 +1197,7 @@
1195 1197 "complex-filter": "Complex filter",
1196 1198 "edit-complex-filter": "Edit complex filter",
1197 1199 "key-filter": "Key filter",
  1200 + "key-filters": "Key filters",
1198 1201 "key-name": "Key name",
1199 1202 "key-name-required": "Key name is required.",
1200 1203 "key-type": {
... ... @@ -1210,6 +1213,8 @@
1210 1213 "boolean": "Boolean"
1211 1214 },
1212 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 1218 "no-key-filters": "No key filters configured",
1214 1219 "add-key-filter": "Add key filter",
1215 1220 "remove-key-filter": "Remove key filter",
... ...
... ... @@ -750,6 +750,9 @@ mat-label {
750 750 &.tb-mat-16 {
751 751 @include tb-mat-icon-size(16);
752 752 }
  753 + &.tb-mat-18 {
  754 + @include tb-mat-icon-size(18);
  755 + }
753 756 &.tb-mat-20 {
754 757 @include tb-mat-icon-size(20);
755 758 }
... ...