Commit 25dae17671511c1fd3a51183c3009b0138ce5e28
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;"> </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;" | ... | ... |
... | ... | @@ -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;"> </span> | |
31 | + <span [fxShow]="disabled || userMode" style="min-width: 40px;"> </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> | ... | ... |
... | ... | @@ -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; | ... | ... |
... | ... | @@ -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", | ... | ... |