diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index d329b12dac..ca29cceb0d 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/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} = {}; @@ -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 { @@ -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]); + } + } } diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index da329a9f12..401adea879 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -68,6 +68,7 @@ export interface EntityDataSubscriptionOptions { isPaginatedDataSubscription?: boolean; pageLink?: EntityDataPageLink; keyFilters?: Array; + additionalKeyFilters?: Array; 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 }; diff --git a/ui-ngx/src/app/core/api/entity-data.service.ts b/ui-ngx/src/app/core/api/entity-data.service.ts index 9159033a92..22f3bfcd15 100644 --- a/ui-ngx/src/app/core/api/entity-data.service.ts +++ b/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 = []; @@ -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, diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 274c807b5c..6f60bc839e 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/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; updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo); + updateUserFilter(filter: Filter); updateEntityAliases(entityAliases: EntityAliases); updateFilters(filters: Filters); updateAliases(aliasIds?: Array); diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index cfcdbebff9..bfed43f3f5 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/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); } } diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index 7c0d13b867..d779a7a9b6 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/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; } diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html index b77de60689..3309a17c94 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html @@ -15,16 +15,16 @@ limitations under the License. --> -
- - filter.operation.operation - +
+ + + {{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} - - {{ 'filter.value' | translate }} + + {{ (booleanFilterPredicateFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts index 889d474bf4..2b54fa0fca 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html index ce17808ca5..d2a874fae9 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

filter.complex-filter

@@ -38,6 +38,7 @@ @@ -45,8 +46,8 @@
-
- filter.no-filters -
- - -
+ + + +
filter.filters
+
+
+
+ +
+
+ + +
+ +   +
+
+ +
+
+
+ {{ complexOperationTranslations.get(operation) | translate }} +
+
+
+ + + +
+
+
+ filter.no-filters +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss new file mode 100644 index 0000000000..335e755433 --- /dev/null +++ b/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; + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts index 9a45ef2eec..08598258e7 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts +++ b/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(); } diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.scss b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.scss new file mode 100644 index 0000000000..b27e3681aa --- /dev/null +++ b/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; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html index 7f8109c722..91bbc11425 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html @@ -32,8 +32,11 @@
- filter.filter - +
filter.filter
+
filter.editable
+
@@ -43,13 +46,15 @@ *ngFor="let filterControl of filtersFormArray().controls; let $index = index"> {{$index + 1}}.
- - - - - {{ 'filter.filter-required' | translate }} - - + {{filterControl.get('filter').value}} +
+ + +
+
+ +
+ diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.scss b/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.scss new file mode 100644 index 0000000000..845b959302 --- /dev/null +++ b/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; + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts new file mode 100644 index 0000000000..1992b5b90c --- /dev/null +++ b/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('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, { + 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); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.html b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.html new file mode 100644 index 0000000000..4b090b214c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.html @@ -0,0 +1,32 @@ + +
+ + + {{ 'filter.filters' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.scss b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.scss new file mode 100644 index 0000000000..a483ad1541 --- /dev/null +++ b/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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts new file mode 100644 index 0000000000..0beb39679f --- /dev/null +++ b/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(); + + 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([ + [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; + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html index 0c210a4538..332f2a1274 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html @@ -15,9 +15,9 @@ limitations under the License. --> - + -

{{data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter'}}

+

{{data.userMode ? data.keyFilter.key.key : ((data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter') | translate)}}

- - - filter.no-key-filters -
- -
+ + + +
filter.key-filters
+
+
+
+ +
+ + +   +   +
+
+ +
+
+
+ filter.operation.and +
+
+
+
{{ keyFilterControl.value.key.key }}
+
{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}
+ + +
+ +
+
+ filter.no-key-filters +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss new file mode 100644 index 0000000000..3951b5d640 --- /dev/null +++ b/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; + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts index 8842dc3020..288485da91 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts +++ b/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; diff --git a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html index c754926684..03ff6b6850 100644 --- a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html @@ -15,17 +15,18 @@ limitations under the License. --> -
- - filter.operation.operation - +
+ + + {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} - - filter.value - + + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts index 71e004c834..1e2e333e1d 100644 --- a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html index a48d8a5f20..a1ca914a7f 100644 --- a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html @@ -15,20 +15,21 @@ limitations under the License. --> -
- - filter.operation.operation - - - {{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}} - - - - - {{ 'filter.ignore-case' | translate }} - - - filter.value - +
+
+ + + + + {{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}} + + + + + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts index bc29782ff9..cdf4de9fec 100644 --- a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 38fadffe33..b4c50462bc 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 3b07bda51e..fd34296423 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/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 { - const singleFilter: Filter = {id: null, filter, keyFilters: []}; + const singleFilter: Filter = {id: null, filter, keyFilters: [], editable: true}; return this.dialog.open(FilterDialogComponent, { disableClose: true, diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html index 0c3d8be048..9f46f6b219 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html @@ -91,6 +91,10 @@ aggregation="true" [(ngModel)]="dashboardCtx.dashboardTimewindow"> + + diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts index 1d2588a91f..8df23a65e8 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts +++ b/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; } diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html index 41a3151d13..082ff62633 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html @@ -59,6 +59,9 @@ {{ 'dashboard.display-entities-selection' | translate }} + + {{ 'dashboard.display-filters' | translate }} + {{ 'dashboard.display-dashboard-timewindow' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.ts index 642a97c1b3..39a19a4bba 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.ts @@ -78,6 +78,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent; } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 3f43233413..af73d52776 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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}}'.
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", diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 44feb80244..1721196b2b 100644 --- a/ui-ngx/src/styles.scss +++ b/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); }