Browse Source

UI: Dashboard filters implementation

pull/3053/head
Igor Kulikov 6 years ago
parent
commit
25dae17671
  1. 24
      ui-ngx/src/app/core/api/alias-controller.ts
  2. 12
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  3. 13
      ui-ngx/src/app/core/api/entity-data.service.ts
  4. 1
      ui-ngx/src/app/core/api/widget-api.models.ts
  5. 2
      ui-ngx/src/app/core/api/widget-subscription.ts
  6. 6
      ui-ngx/src/app/core/services/item-buffer.service.ts
  7. 12
      ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html
  8. 2
      ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts
  9. 7
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html
  10. 5
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts
  11. 3
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.html
  12. 3
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts
  13. 34
      ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html
  14. 28
      ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.scss
  15. 7
      ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts
  16. 106
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html
  17. 29
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss
  18. 12
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts
  19. 29
      ui-ngx/src/app/modules/home/components/filter/filter-predicate.scss
  20. 23
      ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html
  21. 8
      ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.scss
  22. 20
      ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts
  23. 33
      ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.html
  24. 35
      ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.scss
  25. 65
      ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts
  26. 32
      ui-ngx/src/app/modules/home/components/filter/filters-edit.component.html
  27. 36
      ui-ngx/src/app/modules/home/components/filter/filters-edit.component.scss
  28. 154
      ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts
  29. 20
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html
  30. 26
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.scss
  31. 24
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts
  32. 95
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html
  33. 29
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss
  34. 6
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
  35. 15
      ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html
  36. 2
      ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts
  37. 31
      ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html
  38. 2
      ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts
  39. 9
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  40. 2
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  41. 4
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html
  42. 9
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts
  43. 3
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html
  44. 1
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.ts
  45. 1
      ui-ngx/src/app/shared/models/dashboard.models.ts
  46. 1
      ui-ngx/src/app/shared/models/query/query.models.ts
  47. 5
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  48. 3
      ui-ngx/src/styles.scss

24
ui-ngx/src/app/core/api/alias-controller.ts

@ -24,7 +24,7 @@ import { EntityAliases } from '@shared/models/alias.models';
import { EntityInfo } from '@shared/models/entity.models';
import { map, mergeMap } from 'rxjs/operators';
import {
defaultEntityDataPageLink, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink,
defaultEntityDataPageLink, Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink,
updateDatasourceFromEntityInfo
} from '@shared/models/query/query.models';
@ -41,6 +41,7 @@ export class AliasController implements IAliasController {
entityAliases: EntityAliases;
filters: Filters;
userFilters: Filters;
resolvedAliases: {[aliasId: string]: AliasInfo} = {};
resolvedAliasesObservable: {[aliasId: string]: Observable<AliasInfo>} = {};
@ -54,6 +55,7 @@ export class AliasController implements IAliasController {
private origFilters: Filters) {
this.entityAliases = deepClone(this.origEntityAliases);
this.filters = deepClone(this.origFilters);
this.userFilters = {};
}
updateEntityAliases(newEntityAliases: EntityAliases) {
@ -94,6 +96,9 @@ export class AliasController implements IAliasController {
}
this.filters = deepClone(newFilters);
if (changedFilterIds.length) {
for (const filterId of changedFilterIds) {
delete this.userFilters[filterId];
}
this.filtersChangedSubject.next(changedFilterIds);
}
}
@ -146,7 +151,11 @@ export class AliasController implements IAliasController {
}
getFilterInfo(filterId: string): FilterInfo {
return this.filters[filterId];
if (this.userFilters[filterId]) {
return this.userFilters[filterId];
} else {
return this.filters[filterId];
}
}
getKeyFilters(filterId: string): Array<KeyFilter> {
@ -353,4 +362,15 @@ export class AliasController implements IAliasController {
}
}
}
updateUserFilter(filter: Filter) {
let prevUserFilter = this.userFilters[filter.id];
if (!prevUserFilter) {
prevUserFilter = this.filters[filter.id];
}
if (prevUserFilter && !isEqual(prevUserFilter, filter)) {
this.userFilters[filter.id] = filter;
this.filtersChangedSubject.next([filter.id]);
}
}
}

12
ui-ngx/src/app/core/api/entity-data-subscription.ts

@ -68,6 +68,7 @@ export interface EntityDataSubscriptionOptions {
isPaginatedDataSubscription?: boolean;
pageLink?: EntityDataPageLink;
keyFilters?: Array<KeyFilter>;
additionalKeyFilters?: Array<KeyFilter>;
subscriptionTimewindow?: SubscriptionTimewindow;
}
@ -206,10 +207,19 @@ export class EntityDataSubscription {
this.subscriber = new TelemetrySubscriber(this.telemetryService);
this.dataCommand = new EntityDataCmd();
let keyFilters = this.entityDataSubscriptionOptions.keyFilters;
if (this.entityDataSubscriptionOptions.additionalKeyFilters) {
if (keyFilters) {
keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters);
} else {
keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters;
}
}
this.dataCommand.query = {
entityFilter: this.entityDataSubscriptionOptions.entityFilter,
pageLink: this.entityDataSubscriptionOptions.pageLink,
keyFilters: this.entityDataSubscriptionOptions.keyFilters,
keyFilters,
entityFields,
latestValues: this.latestValues
};

13
ui-ngx/src/app/core/api/entity-data.service.ts

@ -65,7 +65,7 @@ export class EntityDataService {
return of(null);
}
listener.subscription = this.createSubscription(listener,
datasource.pageLink, datasource.keyFilters,
datasource.pageLink, datasource.keyFilters, null,
false);
return listener.subscription.subscribe();
}
@ -86,15 +86,8 @@ export class EntityDataService {
if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !pageLink)) {
return of(null);
}
if (datasource.keyFilters) {
if (keyFilters) {
keyFilters = keyFilters.concat(datasource.keyFilters);
} else {
keyFilters = datasource.keyFilters;
}
}
listener.subscription = this.createSubscription(listener,
pageLink, keyFilters, true);
pageLink, datasource.keyFilters, keyFilters,true);
if (listener.subscriptionType === widgetType.timeseries) {
listener.subscription.entityDataSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
}
@ -110,6 +103,7 @@ export class EntityDataService {
private createSubscription(listener: EntityDataListener,
pageLink: EntityDataPageLink,
keyFilters: KeyFilter[],
additionalKeyFilters: KeyFilter[],
isPaginatedDataSubscription: boolean): EntityDataSubscription {
const datasource = listener.configDatasource;
const subscriptionDataKeys: Array<SubscriptionDataKey> = [];
@ -131,6 +125,7 @@ export class EntityDataService {
entityDataSubscriptionOptions.entityFilter = datasource.entityFilter;
entityDataSubscriptionOptions.pageLink = pageLink;
entityDataSubscriptionOptions.keyFilters = keyFilters;
entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters;
}
entityDataSubscriptionOptions.isPaginatedDataSubscription = isPaginatedDataSubscription;
return new EntityDataSubscription(entityDataSubscriptionOptions,

1
ui-ngx/src/app/core/api/widget-api.models.ts

@ -112,6 +112,7 @@ export interface IAliasController {
getFilterInfo(filterId: string): FilterInfo;
getKeyFilters(filterId: string): Array<KeyFilter>;
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo);
updateUserFilter(filter: Filter);
updateEntityAliases(entityAliases: EntityAliases);
updateFilters(filters: Filters);
updateAliases(aliasIds?: Array<string>);

2
ui-ngx/src/app/core/api/widget-subscription.ts

@ -1011,7 +1011,7 @@ export class WidgetSubscription implements IWidgetSubscription {
const entityDataListener = this.entityDataListeners[datasourceIndex];
if (entityDataListener) {
const pageLink = entityDataListener.subscription.entityDataSubscriptionOptions.pageLink;
const keyFilters = entityDataListener.subscription.entityDataSubscriptionOptions.keyFilters;
const keyFilters = entityDataListener.subscription.entityDataSubscriptionOptions.additionalKeyFilters;
this.subscribeForPaginatedData(datasourceIndex, pageLink, keyFilters);
}
}

6
ui-ngx/src/app/core/services/item-buffer.service.ts

@ -410,7 +410,8 @@ export class ItemBufferService {
private prepareFilterInfo(filter: Filter): FilterInfo {
return {
filter: filter.filter,
keyFilters: filter.keyFilters
keyFilters: filter.keyFilters,
editable: filter.editable
};
}
@ -513,7 +514,8 @@ export class ItemBufferService {
if (!newFilterId) {
const newFilterName = this.createFilterName(filters, filterInfo.filter);
newFilterId = this.utils.guid();
filters[newFilterId] = {id: newFilterId, filter: newFilterName, keyFilters: filterInfo.keyFilters};
filters[newFilterId] = {id: newFilterId, filter: newFilterName,
keyFilters: filterInfo.keyFilters, editable: filterInfo.editable};
}
return newFilterId;
}

12
ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html

@ -15,16 +15,16 @@
limitations under the License.
-->
<div fxLayout="row" [formGroup]="booleanFilterPredicateFormGroup">
<mat-form-field class="mat-block">
<mat-label translate>filter.operation.operation</mat-label>
<mat-select required formControlName="operation">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="booleanFilterPredicateFormGroup">
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
<mat-label></mat-label>
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of booleanOperations" [value]="operation">
{{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox formControlName="value">
{{ 'filter.value' | translate }}
<mat-checkbox fxFlex formControlName="value">
{{ (booleanFilterPredicateFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }}
</mat-checkbox>
</div>

2
ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts

@ -26,7 +26,7 @@ import { isDefined } from '@core/utils';
@Component({
selector: 'tb-boolean-filter-predicate',
templateUrl: './boolean-filter-predicate.component.html',
styleUrls: [],
styleUrls: ['./filter-predicate.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,

7
ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<form [formGroup]="complexFilterFormGroup" (ngSubmit)="save()">
<form [formGroup]="complexFilterFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary">
<h2 translate>filter.complex-filter</h2>
<span fxFlex></span>
@ -38,6 +38,7 @@
<tb-filter-predicate-list
[userMode]="data.userMode"
[valueType]="data.valueType"
[operation]="complexFilterFormGroup.get('operation').value"
formControlName="predicates">
</tb-filter-predicate-list>
</fieldset>
@ -45,8 +46,8 @@
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid">
{{ 'action.update' | translate }}
[disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button>
<button mat-button color="primary"
type="button"

5
ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts

@ -33,6 +33,7 @@ export interface ComplexFilterPredicateDialogData {
complexPredicate: ComplexFilterPredicate;
userMode: boolean;
disabled: boolean;
isAdd: boolean;
valueType: EntityKeyValueType;
}
@ -52,6 +53,8 @@ export class ComplexFilterPredicateDialogComponent extends
complexOperationEnum = ComplexOperation;
complexOperationTranslations = complexOperationTranslationMap;
isAdd: boolean;
submitted = false;
constructor(protected store: Store<AppState>,
@ -62,6 +65,8 @@ export class ComplexFilterPredicateDialogComponent extends
private fb: FormBuilder) {
super(store, router, dialogRef);
this.isAdd = this.data.isAdd;
this.complexFilterFormGroup = this.fb.group(
{
operation: [this.data.complexPredicate.operation, [Validators.required]],

3
ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.html

@ -15,9 +15,10 @@
limitations under the License.
-->
<div fxLayout="row">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-label translate>filter.complex-filter</mat-label>
<button mat-icon-button color="primary"
class="tb-mat-32"
[fxShow]="!disabled"
type="button"
(click)="openComplexFilterDialog()"

3
ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts

@ -78,7 +78,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
complexPredicate: deepClone(this.complexFilterPredicate),
disabled: this.disabled,
userMode: this.userMode,
valueType: this.valueType
valueType: this.valueType,
isAdd: false
}
}).afterClosed().subscribe(
(result) => {

34
ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html

@ -15,9 +15,9 @@
limitations under the License.
-->
<form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="min-width: 480px;">
<form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary">
<h2>{{ (isAdd ? 'filter.add' : 'filter.edit') | translate }}</h2>
<h2>{{ userMode ? filter.filter : ((isAdd ? 'filter.add' : 'filter.edit') | translate) }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
@ -30,16 +30,24 @@
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>filter.name</mat-label>
<input matInput formControlName="filter" required>
<mat-error *ngIf="filterFormGroup.get('filter').hasError('required')">
{{ 'filter.name-required' | translate }}
</mat-error>
<mat-error *ngIf="filterFormGroup.get('filter').hasError('duplicateFilterName')">
{{ 'filter.duplicate-filter' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" [fxShow]="!userMode">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>filter.name</mat-label>
<input matInput formControlName="filter" required>
<mat-error *ngIf="filterFormGroup.get('filter').hasError('required')">
{{ 'filter.name-required' | translate }}
</mat-error>
<mat-error *ngIf="filterFormGroup.get('filter').hasError('duplicateFilterName')">
{{ 'filter.duplicate-filter' | translate }}
</mat-error>
</mat-form-field>
<section class="tb-editable-switch" fxLayout="column" fxLayoutAlign="start center">
<label class="tb-small editable-label" translate>filter.editable</label>
<mat-slide-toggle class="editable-switch"
formControlName="editable">
</mat-slide-toggle>
</section>
</div>
<tb-key-filter-list
formControlName="keyFilters"
[userMode]="userMode">
@ -51,7 +59,7 @@
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.update') | translate }}
{{ (userMode ? 'action.update' : (isAdd ? 'action.add' : 'action.update')) | translate }}
</button>
<button mat-button color="primary"
type="button"

28
ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.scss

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-editable-switch {
padding-left: 10px;
.editable-switch {
margin: 0;
}
.editable-label {
margin: 5px 0;
}
}
}

7
ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts

@ -45,7 +45,7 @@ export interface FilterDialogData {
selector: 'tb-filter-dialog',
templateUrl: './filter-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: FilterDialogComponent}],
styleUrls: []
styleUrls: ['./filter-dialog.component.scss']
})
export class FilterDialogComponent extends DialogComponent<FilterDialogComponent, Filter>
implements OnInit, ErrorStateMatcher {
@ -83,7 +83,8 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
this.filter = {
id: null,
filter: '',
keyFilters: []
keyFilters: [],
editable: true
};
} else {
this.filter = data.filter;
@ -91,6 +92,7 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
this.filterFormGroup = this.fb.group({
filter: [this.filter.filter, [this.validateDuplicateFilterName(), Validators.required]],
editable: [this.filter.editable],
keyFilters: [this.filter.keyFilters, Validators.required]
});
}
@ -128,6 +130,7 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
save(): void {
this.submitted = true;
this.filter.filter = this.filterFormGroup.get('filter').value;
this.filter.editable = this.filterFormGroup.get('editable').value;
this.filter.keyFilters = this.filterFormGroup.get('keyFilters').value;
if (this.isAdd) {
this.filter.id = this.utils.guid();

106
ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html

@ -16,42 +16,72 @@
-->
<section fxLayout="column" [formGroup]="filterListFormGroup">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" style="max-height: 40px;"
formArrayName="predicates"
*ngFor="let predicateControl of predicatesFormArray().controls; let $index = index">
<tb-filter-predicate
[userMode]="userMode"
[valueType]="valueType"
[formControl]="predicateControl">
</tb-filter-predicate>
<button mat-icon-button color="primary"
[fxShow]="!disabled && !userMode"
type="button"
(click)="removePredicate($index)"
matTooltip="{{ 'filter.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
<span [fxShow]="!predicatesFormArray().length"
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
class="no-data-found" translate>filter.no-filters</span>
<div style="margin-top: 8px;" fxLayout="row" fxLayoutGap="8px">
<button mat-button mat-raised-button color="primary"
[fxShow]="!disabled && !userMode"
(click)="addPredicate(false)"
type="button"
matTooltip="{{ 'filter.add-filter' | translate }}"
matTooltipPosition="above">
{{ 'action.add' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
[fxShow]="!disabled && !userMode"
(click)="addPredicate(true)"
type="button"
matTooltip="{{ 'filter.add-complex-filter' | translate }}"
matTooltipPosition="above">
{{ 'filter.add-complex' | translate }}
</button>
</div>
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div translate>filter.filters</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="row">
<span fxFlex="8"></span>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex="92">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<label fxFlex translate class="tb-title no-padding">filter.operation.operation</label>
<label *ngIf="valueType === valueTypeEnum.STRING"
translate class="tb-title no-padding" style="min-width: 70px;">filter.ignore-case</label>
</div>
<label fxFlex translate class="tb-title no-padding">filter.value</label>
<span [fxShow]="!disabled && !userMode" style="min-width: 40px;">&nbsp;</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="predicate-list">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;"
formArrayName="predicates"
*ngFor="let predicateControl of predicatesFormArray().controls; let $index = index">
<div fxFlex="8" fxLayout="row" fxLayoutAlign="center" class="filters-operation">
<span *ngIf="$index > 0">{{ complexOperationTranslations.get(operation) | translate }}</span>
</div>
<div fxLayout="column" fxFlex="92">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex>
<tb-filter-predicate
fxFlex
[userMode]="userMode"
[valueType]="valueType"
[formControl]="predicateControl">
</tb-filter-predicate>
<button mat-icon-button color="primary"
[fxShow]="!disabled && !userMode"
type="button"
(click)="removePredicate($index)"
matTooltip="{{ 'filter.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</div>
<span [fxShow]="!predicatesFormArray().length"
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
class="no-data-found" translate>filter.no-filters</span>
</div>
<div style="margin-top: 16px;" fxLayout="row" fxLayoutGap="8px">
<button mat-button mat-raised-button color="primary"
[fxShow]="!disabled && !userMode"
(click)="addPredicate(false)"
type="button"
matTooltip="{{ 'filter.add-filter' | translate }}"
matTooltipPosition="above">
{{ 'action.add' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
[fxShow]="!disabled && !userMode"
(click)="addPredicate(true)"
type="button"
matTooltip="{{ 'filter.add-complex-filter' | translate }}"
matTooltipPosition="above">
{{ 'filter.add-complex' | translate }}
</button>
</div>
</mat-expansion-panel>
</section>

29
ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.predicate-list {
overflow: auto;
max-height: 350px;
.no-data-found {
height: 50px;
}
}
.filters-operation {
margin-top: -40px;
color: #666;
font-weight: 500;
}
}

12
ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts

@ -27,6 +27,7 @@ import {
import { Observable, of, Subscription } from 'rxjs';
import {
ComplexFilterPredicate,
ComplexOperation, complexOperationTranslationMap,
createDefaultFilterPredicate,
EntityKeyValueType,
KeyFilterPredicate
@ -40,7 +41,7 @@ import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'tb-filter-predicate-list',
templateUrl: './filter-predicate-list.component.html',
styleUrls: [],
styleUrls: ['./filter-predicate-list.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -57,8 +58,14 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
@Input() valueType: EntityKeyValueType;
@Input() operation: ComplexOperation = ComplexOperation.AND;
filterListFormGroup: FormGroup;
valueTypeEnum = EntityKeyValueType;
complexOperationTranslations = complexOperationTranslationMap;
private propagateChange = null;
private valueChangeSubscription: Subscription = null;
@ -143,7 +150,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
complexPredicate: predicate,
disabled: this.disabled,
userMode: this.userMode,
valueType: this.valueType
valueType: this.valueType,
isAdd: true
}
}).afterClosed();
}

29
ui-ngx/src/app/modules/home/components/filter/filter-predicate.scss

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
mat-form-field {
.mat-form-field-wrapper {
padding-bottom: 0;
.mat-form-field-infix {
border-top-width: 0.2em;
width: auto;
}
.mat-form-field-underline {
bottom: 0;
}
}
}
}

23
ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html

@ -32,8 +32,11 @@
<div class="tb-filters-header" fxLayout="row" fxLayoutAlign="start center">
<span fxFlex="5"></span>
<div fxFlex="95" fxLayout="row" fxLayoutAlign="start center">
<span class="tb-header-label" translate fxFlex>filter.filter</span>
<span style="min-width: 80px;"></span>
<div class="tb-header-label" translate fxFlex>filter.filter</div>
<div class="tb-header-label" translate fxFlex="120px"
fxLayout="column" fxLayoutAlign="center center"
style="padding-left: 30px;">filter.editable</div>
<div style="min-width: 80px;"></div>
</div>
</div>
<fieldset [disabled]="isLoading$ | async">
@ -43,13 +46,15 @@
*ngFor="let filterControl of filtersFormArray().controls; let $index = index">
<span fxFlex="5">{{$index + 1}}.</span>
<div class="mat-elevation-z4 tb-filter" fxFlex="95" fxLayout="row" fxLayoutAlign="start center">
<mat-form-field floatLabel="always" hideRequiredMarker class="mat-block" fxFlex>
<mat-label></mat-label>
<input matInput [formControl]="filterControl.get('filter')" required placeholder="{{ 'filter.filter' | translate }}">
<mat-error *ngIf="filterControl.get('filter').hasError('required')">
{{ 'filter.filter-required' | translate }}
</mat-error>
</mat-form-field>
<mat-label fxFlex>{{filterControl.get('filter').value}}</mat-label>
<section fxFlex="120px" style="padding-left: 10px;"
class="tb-editable-switch"
fxLayout="column"
fxLayoutAlign="center center">
<mat-slide-toggle class="editable-switch"
[formControl]="filterControl.get('editable')">
</mat-slide-toggle>
</section>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 40px;"

8
ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.scss

@ -33,6 +33,14 @@
.tb-filter {
padding: 0 0 0 10px;
margin: 5px;
.tb-editable-switch {
padding-left: 10px;
.editable-switch {
margin: 0;
}
}
}
}

20
ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts

@ -36,7 +36,7 @@ import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogService } from '@core/services/dialog.service';
import { deepClone } from '@core/utils';
import { deepClone, isUndefined } from '@core/utils';
import { Filter, Filters, KeyFilterInfo } from '@shared/models/query/query.models';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
@ -109,6 +109,9 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
const filterControls: Array<AbstractControl> = [];
for (const filterId of Object.keys(this.data.filters)) {
const filter = this.data.filters[filterId];
if (isUndefined(filter.editable)) {
filter.editable = true;
}
filterControls.push(this.createFilterFormControl(filterId, filter));
}
@ -121,7 +124,8 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
const filterFormControl = this.fb.group({
id: [filterId],
filter: [filter ? filter.filter : null, [Validators.required]],
keyFilters: [filter ? filter.keyFilters : [], [Validators.required]]
keyFilters: [filter ? filter.keyFilters : [], [Validators.required]],
editable: [filter ? filter.editable : true]
});
return filterFormControl;
}
@ -148,9 +152,9 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
for (const widgetTitle of widgetsTitleList) {
widgetsListHtml += '<br/>\'' + widgetTitle + '\'';
}
const message = this.translate.instant('entity.unable-delete-filter-text',
const message = this.translate.instant('filter.unable-delete-filter-text',
{filter: filter.filter, widgetsList: widgetsListHtml});
this.dialogs.alert(this.translate.instant('entity.unable-delete-filter-title'),
this.dialogs.alert(this.translate.instant('filter.unable-delete-filter-title'),
message, this.translate.instant('action.close'), true);
} else {
(this.filtersFormGroup.get('filters') as FormArray).removeAt(index);
@ -190,8 +194,9 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
.push(this.createFilterFormControl(result.id, result));
} else {
const filterFormControl = (this.filtersFormGroup.get('filters') as FormArray).at(index);
filterFormControl.get('filter').patchValue(filter.filter);
filterFormControl.get('keyFilters').patchValue(filter.keyFilters);
filterFormControl.get('filter').patchValue(result.filter);
filterFormControl.get('editable').patchValue(result.editable);
filterFormControl.get('keyFilters').patchValue(result.keyFilters);
}
this.filtersFormGroup.markAsDirty();
}
@ -215,6 +220,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
const filterId: string = filterValue.id;
const filter: string = filterValue.filter;
const keyFilters: Array<KeyFilterInfo> = filterValue.keyFilters;
const editable: boolean = filterValue.editable;
if (uniqueFilterList[filter]) {
valid = false;
message = this.translate.instant('filter.duplicate-filter-error', {filter});
@ -225,7 +231,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
break;
} else {
uniqueFilterList[filter] = filter;
filters[filterId] = {id: filterId, filter, keyFilters};
filters[filterId] = {id: filterId, filter, keyFilters, editable};
}
}
if (valid) {

33
ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.html

@ -0,0 +1,33 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div fxLayout="column" class="mat-content mat-padding">
<div fxLayout="column" *ngFor="let filter of filtersInfo | keyvalue">
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<mat-label fxFlex>{{filter.value.filter}}</mat-label>
<button mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="editFilter(filter.key, filter.value)"
matTooltip="{{ 'filter.edit' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
</div>
<mat-divider></mat-divider>
</div>
</div>

35
ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.scss

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
min-width: 300px;
max-height: 150px;
overflow-x: hidden;
overflow-y: auto;
background: #fff;
border-radius: 4px;
box-shadow:
0 7px 8px -4px rgba(0, 0, 0, .2),
0 13px 19px 2px rgba(0, 0, 0, .14),
0 5px 24px 4px rgba(0, 0, 0, .12);
@media (min-height: 350px) {
max-height: 250px;
}
.mat-content {
background-color: #fff;
}
}

65
ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts

@ -0,0 +1,65 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, InjectionToken } from '@angular/core';
import { IAliasController } from '@core/api/widget-api.models';
import { Filter, FilterInfo } from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
import { deepClone } from '@core/utils';
export const FILTER_EDIT_PANEL_DATA = new InjectionToken<any>('FiltersEditPanelData');
export interface FiltersEditPanelData {
aliasController: IAliasController;
filtersInfo: {[filterId: string]: FilterInfo};
}
@Component({
selector: 'tb-filters-edit-panel',
templateUrl: './filters-edit-panel.component.html',
styleUrls: ['./filters-edit-panel.component.scss']
})
export class FiltersEditPanelComponent {
filtersInfo: {[filterId: string]: FilterInfo};
constructor(@Inject(FILTER_EDIT_PANEL_DATA) public data: FiltersEditPanelData,
private dialog: MatDialog) {
this.filtersInfo = this.data.filtersInfo;
}
public editFilter(filterId: string, filter: FilterInfo) {
const singleFilter: Filter = {id: filterId, ...deepClone(filter)};
this.dialog.open<FilterDialogComponent, FilterDialogData,
Filter>(FilterDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd: false,
filters: [],
filter: singleFilter,
userMode: true
}
}).afterClosed().subscribe(
(result) => {
if (result) {
this.filtersInfo[result.id] = result;
this.data.aliasController.updateUserFilter(result);
}
});
}
}

32
ui-ngx/src/app/modules/home/components/filter/filters-edit.component.html

@ -0,0 +1,32 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section class="tb-filters-edit" fxLayout="row" fxLayoutAlign="start center" *ngIf="hasEditableFilters">
<button mat-icon-button
cdkOverlayOrigin #filtersEditPanelOrigin="cdkOverlayOrigin"
(click)="openEditMode()"
matTooltip="{{ 'filter.filters' | translate }}"
[matTooltipPosition]="tooltipPosition">
<mat-icon>filter_list</mat-icon>
</button>
<span fxHide.lt-lg
(click)="openEditMode()"
matTooltip="{{ 'filter.filters' | translate }}"
[matTooltipPosition]="tooltipPosition">
{{ 'filter.filters' | translate }}
</span>
</section>

36
ui-ngx/src/app/modules/home/components/filter/filters-edit.component.scss

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../../scss/constants";
:host {
section.tb-filters-edit {
min-height: 32px;
padding: 0 6px;
@media #{$mat-lt-md} {
padding: 0;
}
span {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: all;
cursor: pointer;
}
}
}

154
ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts

@ -0,0 +1,154 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { TooltipPosition } from '@angular/material/tooltip';
import { IAliasController } from '@core/api/widget-api.models';
import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { BreakpointObserver } from '@angular/cdk/layout';
import { deepClone } from '@core/utils';
import { FilterInfo } from '@shared/models/query/query.models';
import {
FILTER_EDIT_PANEL_DATA,
FiltersEditPanelComponent,
FiltersEditPanelData
} from '@home/components/filter/filters-edit-panel.component';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
@Component({
selector: 'tb-filters-edit',
templateUrl: './filters-edit.component.html',
styleUrls: ['./filters-edit.component.scss']
})
export class FiltersEditComponent implements OnInit, OnDestroy {
aliasControllerValue: IAliasController;
@Input()
set aliasController(aliasController: IAliasController) {
this.aliasControllerValue = aliasController;
this.setupAliasController(this.aliasControllerValue);
}
get aliasController(): IAliasController {
return this.aliasControllerValue;
}
@Input()
tooltipPosition: TooltipPosition = 'above';
@Input() disabled: boolean;
@ViewChild('filtersEditPanelOrigin') filtersEditPanelOrigin: CdkOverlayOrigin;
displayValue: string;
filtersInfo: {[filterId: string]: FilterInfo} = {};
hasEditableFilters = false;
private rxSubscriptions = new Array<Subscription>();
constructor(private translate: TranslateService,
private overlay: Overlay,
private breakpointObserver: BreakpointObserver,
private viewContainerRef: ViewContainerRef) {
}
private setupAliasController(aliasController: IAliasController) {
this.rxSubscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
this.rxSubscriptions.length = 0;
if (aliasController) {
this.rxSubscriptions.push(aliasController.filtersChanged.subscribe(
() => {
setTimeout(() => {
this.updateFiltersInfo();
}, 0);
}
));
setTimeout(() => {
this.updateFiltersInfo();
}, 0);
}
}
ngOnInit(): void {
}
ngOnDestroy(): void {
this.rxSubscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
this.rxSubscriptions.length = 0;
}
openEditMode() {
if (this.disabled || !this.hasEditableFilters) {
return;
}
const position = this.overlay.position();
const config = new OverlayConfig({
panelClass: 'tb-filters-edit-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
});
const connectedPosition: ConnectedPosition = {
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top'
};
config.positionStrategy = position.flexibleConnectedTo(this.filtersEditPanelOrigin.elementRef)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const injector = this._createFiltersEditPanelInjector(
overlayRef,
{
aliasController: this.aliasController,
filtersInfo: deepClone(this.filtersInfo)
}
);
overlayRef.attach(new ComponentPortal(FiltersEditPanelComponent, this.viewContainerRef, injector));
}
private _createFiltersEditPanelInjector(overlayRef: OverlayRef, data: FiltersEditPanelData): PortalInjector {
const injectionTokens = new WeakMap<any, any>([
[FILTER_EDIT_PANEL_DATA, data],
[OverlayRef, overlayRef]
]);
return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
}
private updateFiltersInfo() {
const allFilters = this.aliasController.getFilters();
this.filtersInfo = {};
this.hasEditableFilters = false;
for (const filterId of Object.keys(allFilters)) {
const filterInfo = this.aliasController.getFilterInfo(filterId);
if (filterInfo && filterInfo.editable) {
this.filtersInfo[filterId] = deepClone(filterInfo);
this.hasEditableFilters = true;
}
}
}
}

20
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html

@ -15,9 +15,9 @@
limitations under the License.
-->
<form [formGroup]="keyFilterFormGroup" (ngSubmit)="save()">
<form [formGroup]="keyFilterFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary">
<h2 translate>{{data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter'}}</h2>
<h2>{{data.userMode ? data.keyFilter.key.key : ((data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter') | translate)}}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
@ -27,16 +27,16 @@
</mat-toolbar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" fxLayout="column">
<section fxLayout="row" fxLayoutGap="8px">
<section fxFlex="60" fxLayout="row" formGroupName="key" fxLayoutGap="8px">
<mat-form-field fxFlex="40" class="mat-block">
<section fxLayout="row" fxLayoutGap="8px" class="entity-key" [fxShow]="!data.userMode">
<section fxFlex="70" fxLayout="row" formGroupName="key" fxLayoutGap="8px">
<mat-form-field fxFlex="60" class="mat-block">
<mat-label translate>filter.key-name</mat-label>
<input matInput required formControlName="key">
<mat-error *ngIf="keyFilterFormGroup.get('key.key').hasError('required')">
{{ 'filter.key-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="60" class="mat-block">
<mat-form-field fxFlex="40" class="mat-block">
<mat-label translate>filter.key-type.key-type</mat-label>
<mat-select required formControlName="type">
<mat-option *ngFor="let type of entityKeyTypes" [value]="type">
@ -45,15 +45,15 @@
</mat-select>
</mat-form-field>
</section>
<mat-form-field fxFlex="40" class="mat-block">
<mat-form-field fxFlex="30" class="mat-block">
<mat-label translate>filter.value-type.value-type</mat-label>
<mat-select matInput formControlName="valueType">
<mat-select-trigger>
<mat-icon svgIcon="{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.icon }}"></mat-icon>
<mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.icon }}"></mat-icon>
<span>{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }}</span>
</mat-select-trigger>
<mat-option *ngFor="let valueType of entityKeyValueTypesKeys" [value]="valueType">
<mat-icon svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon>
<mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon>
<span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span>
</mat-option>
</mat-select>
@ -72,7 +72,7 @@
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid">
[disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid || !keyFilterFormGroup.dirty">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button>
<button mat-button color="primary"

26
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.scss

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
.entity-key {
mat-form-field {
.mat-form-field-wrapper {
.mat-form-field-infix {
width: auto;
}
}
}
}
}

24
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts

@ -27,8 +27,10 @@ import {
entityKeyTypeTranslationMap,
EntityKeyValueType,
entityKeyValueTypesMap,
KeyFilterInfo
KeyFilterInfo, KeyFilterPredicate
} from '@shared/models/query/query.models';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
export interface KeyFilterDialogData {
keyFilter: KeyFilterInfo;
@ -40,7 +42,7 @@ export interface KeyFilterDialogData {
selector: 'tb-key-filter-dialog',
templateUrl: './key-filter-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: KeyFilterDialogComponent}],
styleUrls: []
styleUrls: ['./key-filter-dialog.component.scss']
})
export class KeyFilterDialogComponent extends
DialogComponent<KeyFilterDialogComponent, KeyFilterInfo>
@ -65,6 +67,8 @@ export class KeyFilterDialogComponent extends
@Inject(MAT_DIALOG_DATA) public data: KeyFilterDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<KeyFilterDialogComponent, KeyFilterInfo>,
private dialogs: DialogService,
private translate: TranslateService,
private fb: FormBuilder) {
super(store, router, dialogRef);
@ -80,6 +84,22 @@ export class KeyFilterDialogComponent extends
predicates: [this.data.keyFilter.predicates, [Validators.required]]
}
);
this.keyFilterFormGroup.get('valueType').valueChanges.subscribe((valueType: EntityKeyValueType) => {
const prevValue: EntityKeyValueType = this.keyFilterFormGroup.value.valueType;
const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value;
if (prevValue && prevValue !== valueType && predicates && predicates.length) {
this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'),
this.translate.instant('filter.key-value-type-change-message')).subscribe(
(result) => {
if (result) {
this.keyFilterFormGroup.get('predicates').setValue([]);
} else {
this.keyFilterFormGroup.get('valueType').setValue(prevValue, {emitEvent: false});
}
}
);
}
});
}
ngOnInit(): void {

95
ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html

@ -16,38 +16,65 @@
-->
<section fxLayout="column" [formGroup]="keyFilterListFormGroup">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" style="max-height: 40px;"
formArrayName="keyFilters"
*ngFor="let keyFilterControl of keyFiltersFormArray().controls; let $index = index">
<span>{{ keyFilterControl.value.key.key }}</span>
<span>{{ keyFilterControl.value.key.type }}</span>
<button mat-icon-button color="primary"
type="button"
(click)="editKeyFilter($index)"
matTooltip="{{ 'filter.edit-key-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="primary"
[fxShow]="!disabled && !userMode"
type="button"
(click)="removeKeyFilter($index)"
matTooltip="{{ 'filter.remove-key-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
<span [fxShow]="!keyFiltersFormArray().length"
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
class="no-data-found" translate>filter.no-key-filters</span>
<div style="margin-top: 8px;">
<button mat-button mat-raised-button color="primary"
[fxShow]="!disabled && !userMode"
(click)="addKeyFilter()"
type="button"
matTooltip="{{ 'filter.add-key-filter' | translate }}"
matTooltipPosition="above">
{{ 'filter.add-key-filter' | translate }}
</button>
</div>
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div translate>filter.key-filters</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="row">
<span fxFlex="8"></span>
<div fxLayout="row" fxLayoutAlign="start center" fxFlex="92">
<label fxFlex translate class="tb-title no-padding">filter.key-name</label>
<label fxFlex translate class="tb-title no-padding">filter.key-type.key-type</label>
<span [fxShow]="!disabled && !userMode" style="min-width: 80px;">&nbsp;</span>
<span [fxShow]="disabled || userMode" style="min-width: 40px;">&nbsp;</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="key-filter-list">
<div fxLayout="row" fxLayoutAlign="start center" style="max-height: 40px;"
formArrayName="keyFilters"
*ngFor="let keyFilterControl of keyFiltersFormArray().controls; let $index = index">
<div fxFlex="8" class="filters-operation">
<span *ngIf="$index > 0" translate>filter.operation.and</span>
</div>
<div fxLayout="column" fxFlex="92">
<div fxLayout="row" fxLayoutAlign="start center" fxFlex>
<div fxFlex>{{ keyFilterControl.value.key.key }}</div>
<div fxFlex translate>{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}</div>
<button mat-icon-button color="primary"
type="button"
(click)="editKeyFilter($index)"
matTooltip="{{ 'filter.edit-key-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="primary"
[fxShow]="!disabled && !userMode"
type="button"
(click)="removeKeyFilter($index)"
matTooltip="{{ 'filter.remove-key-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
<mat-divider></mat-divider>
</div>
</div>
<span [fxShow]="!keyFiltersFormArray().length"
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
class="no-data-found" translate>filter.no-key-filters</span>
</div>
<div style="margin-top: 16px;">
<button mat-button mat-raised-button color="primary"
[fxShow]="!disabled && !userMode"
(click)="addKeyFilter()"
type="button"
matTooltip="{{ 'filter.add-key-filter' | translate }}"
matTooltipPosition="above">
{{ 'filter.add-key-filter' | translate }}
</button>
</div>
</mat-expansion-panel>
</section>

29
ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.key-filter-list {
overflow: auto;
max-height: 300px;
.no-data-found {
height: 50px;
}
}
.filters-operation {
margin-top: -40px;
color: #666;
font-weight: 500;
}
}

6
ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts

@ -25,7 +25,7 @@ import {
Validators
} from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { EntityKeyType, KeyFilterInfo } from '@shared/models/query/query.models';
import { EntityKeyType, entityKeyTypeTranslationMap, KeyFilterInfo } from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog';
import { deepClone } from '@core/utils';
import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component';
@ -33,7 +33,7 @@ import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/
@Component({
selector: 'tb-key-filter-list',
templateUrl: './key-filter-list.component.html',
styleUrls: [],
styleUrls: ['./key-filter-list.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -50,6 +50,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
keyFilterListFormGroup: FormGroup;
entityKeyTypeTranslations = entityKeyTypeTranslationMap;
private propagateChange = null;
private valueChangeSubscription: Subscription = null;

15
ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html

@ -15,17 +15,18 @@
limitations under the License.
-->
<div fxLayout="row" [formGroup]="numericFilterPredicateFormGroup">
<mat-form-field class="mat-block">
<mat-label translate>filter.operation.operation</mat-label>
<mat-select required formControlName="operation">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="numericFilterPredicateFormGroup">
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
<mat-label></mat-label>
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of numericOperations" [value]="operation">
{{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>filter.value</mat-label>
<input required type="number" matInput formControlName="value">
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
<mat-label></mat-label>
<input required type="number" matInput formControlName="value"
placeholder="{{'filter.value' | translate}}">
</mat-form-field>
</div>

2
ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts

@ -24,7 +24,7 @@ import { isDefined } from '@core/utils';
@Component({
selector: 'tb-numeric-filter-predicate',
templateUrl: './numeric-filter-predicate.component.html',
styleUrls: [],
styleUrls: ['./filter-predicate.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,

31
ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html

@ -15,20 +15,21 @@
limitations under the License.
-->
<div fxLayout="row" [formGroup]="stringFilterPredicateFormGroup">
<mat-form-field class="mat-block">
<mat-label translate>filter.operation.operation</mat-label>
<mat-select required formControlName="operation">
<mat-option *ngFor="let operation of stringOperations" [value]="operation">
{{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox formControlName="ignoreCase">
{{ 'filter.ignore-case' | translate }}
</mat-checkbox>
<mat-form-field class="mat-block">
<mat-label translate>filter.value</mat-label>
<input matInput formControlName="value">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="stringFilterPredicateFormGroup">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
<mat-label></mat-label>
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of stringOperations" [value]="operation">
{{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox fxLayout="row" fxLayoutAlign="center" formControlName="ignoreCase" style="min-width: 70px;">
</mat-checkbox>
</div>
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
<mat-label></mat-label>
<input matInput formControlName="value" placeholder="{{'filter.value' | translate}}">
</mat-form-field>
</div>

2
ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts

@ -26,7 +26,7 @@ import {
@Component({
selector: 'tb-string-filter-predicate',
templateUrl: './string-filter-predicate.component.html',
styleUrls: [],
styleUrls: ['./filter-predicate.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,

9
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -78,6 +78,8 @@ import { KeyFilterDialogComponent } from '@home/components/filter/key-filter-dia
import { FiltersDialogComponent } from '@home/components/filter/filters-dialog.component';
import { FilterDialogComponent } from '@home/components/filter/filter-dialog.component';
import { FilterSelectComponent } from './filter/filter-select.component';
import { FiltersEditComponent } from '@home/components/filter/filters-edit.component';
import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.component';
@NgModule({
declarations:
@ -138,7 +140,9 @@ import { FilterSelectComponent } from './filter/filter-select.component';
KeyFilterDialogComponent,
FilterDialogComponent,
FiltersDialogComponent,
FilterSelectComponent
FilterSelectComponent,
FiltersEditComponent,
FiltersEditPanelComponent
],
imports: [
CommonModule,
@ -192,7 +196,8 @@ import { FilterSelectComponent } from './filter/filter-select.component';
KeyFilterDialogComponent,
FilterDialogComponent,
FiltersDialogComponent,
FilterSelectComponent
FilterSelectComponent,
FiltersEditComponent
],
providers: [
WidgetComponentService,

2
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -721,7 +721,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
private createFilter(filter: string): Observable<Filter> {
const singleFilter: Filter = {id: null, filter, keyFilters: []};
const singleFilter: Filter = {id: null, filter, keyFilters: [], editable: true};
return this.dialog.open<FilterDialogComponent, FilterDialogData,
Filter>(FilterDialogComponent, {
disableClose: true,

4
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html

@ -91,6 +91,10 @@
aggregation="true"
[(ngModel)]="dashboardCtx.dashboardTimewindow">
</tb-timewindow>
<tb-filters-edit [fxShow]="!isEdit && displayFilters()"
tooltipPosition="below"
[aliasController]="dashboardCtx.aliasController">
</tb-filters-edit>
<tb-aliases-entity-select [fxShow]="!isEdit && displayEntitiesSelect()"
tooltipPosition="below"
[aliasController]="dashboardCtx.aliasController">

9
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts

@ -410,6 +410,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
}
public displayFilters(): boolean {
if (this.dashboard.configuration.settings &&
isDefined(this.dashboard.configuration.settings.showFilters)) {
return this.dashboard.configuration.settings.showFilters;
} else {
return true;
}
}
public showRightLayoutSwitch(): boolean {
return this.isMobile && this.layouts.right.show;
}

3
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html

@ -59,6 +59,9 @@
<mat-checkbox fxFlex formControlName="showEntitiesSelect">
{{ 'dashboard.display-entities-selection' | translate }}
</mat-checkbox>
<mat-checkbox fxFlex formControlName="showFilters">
{{ 'dashboard.display-filters' | translate }}
</mat-checkbox>
<mat-checkbox fxFlex formControlName="showDashboardTimewindow">
{{ 'dashboard.display-dashboard-timewindow' | translate }}
</mat-checkbox>

1
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.ts

@ -78,6 +78,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
titleColor: [isUndefined(this.settings.titleColor) ? 'rgba(0,0,0,0.870588)' : this.settings.titleColor, []],
showDashboardsSelect: [isUndefined(this.settings.showDashboardsSelect) ? true : this.settings.showDashboardsSelect, []],
showEntitiesSelect: [isUndefined(this.settings.showEntitiesSelect) ? true : this.settings.showEntitiesSelect, []],
showFilters: [isUndefined(this.settings.showFilters) ? true : this.settings.showFilters, []],
showDashboardTimewindow: [isUndefined(this.settings.showDashboardTimewindow) ? true : this.settings.showDashboardTimewindow, []],
showDashboardExport: [isUndefined(this.settings.showDashboardExport) ? true : this.settings.showDashboardExport, []]
});

1
ui-ngx/src/app/shared/models/dashboard.models.ts

@ -85,6 +85,7 @@ export interface DashboardSettings {
showTitle?: boolean;
showDashboardsSelect?: boolean;
showEntitiesSelect?: boolean;
showFilters?: boolean;
showDashboardTimewindow?: boolean;
showDashboardExport?: boolean;
toolbarAlwaysOpen?: boolean;

1
ui-ngx/src/app/shared/models/query/query.models.ts

@ -248,6 +248,7 @@ export interface KeyFilterInfo {
export interface FilterInfo {
filter: string;
editable: boolean;
keyFilters: Array<KeyFilterInfo>;
}

5
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -549,6 +549,7 @@
"title-color": "Title color",
"display-dashboards-selection": "Display dashboards selection",
"display-entities-selection": "Display entities selection",
"display-filters": "Display filters",
"display-dashboard-timewindow": "Display timewindow",
"display-dashboard-export": "Display export",
"import": "Import dashboard",
@ -1166,6 +1167,7 @@
"duplicate-filter-error": "Duplicate filter found '{{filter}}'.<br>Filters must be unique within the dashboard.",
"missing-key-filters-error": "Key filters is missing for filter '{{filter}}'.",
"filter": "Filter",
"editable": "Editable",
"no-filters-found": "No filters found.",
"no-filter-matching": "'{{filter}}' not found.",
"create-new-filter": "Create a new one!",
@ -1195,6 +1197,7 @@
"complex-filter": "Complex filter",
"edit-complex-filter": "Edit complex filter",
"key-filter": "Key filter",
"key-filters": "Key filters",
"key-name": "Key name",
"key-name-required": "Key name is required.",
"key-type": {
@ -1210,6 +1213,8 @@
"boolean": "Boolean"
},
"value-type-required": "Key value type is required.",
"key-value-type-change-title": "Are you sure you want to change key value type?",
"key-value-type-change-message": "If you confirm new value type all entered key filters will be removed.",
"no-key-filters": "No key filters configured",
"add-key-filter": "Add key filter",
"remove-key-filter": "Remove key filter",

3
ui-ngx/src/styles.scss

@ -750,6 +750,9 @@ mat-label {
&.tb-mat-16 {
@include tb-mat-icon-size(16);
}
&.tb-mat-18 {
@include tb-mat-icon-size(18);
}
&.tb-mat-20 {
@include tb-mat-icon-size(20);
}

Loading…
Cancel
Save