Browse Source

UI: Data query - key filters initial implementation.

pull/3053/head
Igor Kulikov 6 years ago
parent
commit
9600cc0420
  1. 51
      ui-ngx/src/app/core/api/alias-controller.ts
  2. 6
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  3. 7
      ui-ngx/src/app/core/api/entity-data.service.ts
  4. 16
      ui-ngx/src/app/core/api/widget-api.models.ts
  5. 45
      ui-ngx/src/app/core/api/widget-subscription.ts
  6. 3
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  7. 107
      ui-ngx/src/app/core/services/item-buffer.service.ts
  8. 3
      ui-ngx/src/app/core/services/utils.service.ts
  9. 5
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html
  10. 3
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts
  11. 6
      ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.ts
  12. 5
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  13. 30
      ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html
  14. 101
      ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts
  15. 59
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html
  16. 94
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts
  17. 28
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.html
  18. 97
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts
  19. 63
      ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html
  20. 137
      ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts
  21. 57
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html
  22. 159
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts
  23. 42
      ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html
  24. 93
      ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts
  25. 61
      ui-ngx/src/app/modules/home/components/filter/filter-select.component.html
  26. 22
      ui-ngx/src/app/modules/home/components/filter/filter-select.component.models.ts
  27. 24
      ui-ngx/src/app/modules/home/components/filter/filter-select.component.scss
  28. 249
      ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts
  29. 99
      ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html
  30. 44
      ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.scss
  31. 241
      ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts
  32. 86
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html
  33. 105
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts
  34. 53
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html
  35. 165
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
  36. 31
      ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html
  37. 99
      ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts
  38. 34
      ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html
  39. 104
      ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts
  40. 40
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  41. 23
      ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts
  42. 8
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts
  43. 2
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  44. 28
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  45. 3
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts
  46. 10
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
  47. 31
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  48. 14
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  49. 1
      ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.html
  50. 6
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html
  51. 35
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts
  52. 1
      ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.html
  53. 1
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts
  54. 2
      ui-ngx/src/app/shared/models/dashboard.models.ts
  55. 157
      ui-ngx/src/app/shared/models/query/query.models.ts
  56. 1
      ui-ngx/src/app/shared/models/widget.models.ts
  57. 61
      ui-ngx/src/assets/locale/locale.constant-en_US.json

51
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, singleEntityDataPageLink,
defaultEntityDataPageLink, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink,
updateDatasourceFromEntityInfo
} from '@shared/models/query/query.models';
@ -33,10 +33,14 @@ export class AliasController implements IAliasController {
entityAliasesChangedSubject = new Subject<Array<string>>();
entityAliasesChanged: Observable<Array<string>> = this.entityAliasesChangedSubject.asObservable();
filtersChangedSubject = new Subject<Array<string>>();
filtersChanged: Observable<Array<string>> = this.filtersChangedSubject.asObservable();
private entityAliasResolvedSubject = new Subject<string>();
entityAliasResolved: Observable<string> = this.entityAliasResolvedSubject.asObservable();
entityAliases: EntityAliases;
filters: Filters;
resolvedAliases: {[aliasId: string]: AliasInfo} = {};
resolvedAliasesObservable: {[aliasId: string]: Observable<AliasInfo>} = {};
@ -46,11 +50,12 @@ export class AliasController implements IAliasController {
constructor(private utils: UtilsService,
private entityService: EntityService,
private stateControllerHolder: StateControllerHolder,
private origEntityAliases: EntityAliases) {
private origEntityAliases: EntityAliases,
private origFilters: Filters) {
this.entityAliases = deepClone(this.origEntityAliases);
this.filters = deepClone(this.origFilters);
}
updateEntityAliases(newEntityAliases: EntityAliases) {
const changedAliasIds: Array<string> = [];
for (const aliasId of Object.keys(newEntityAliases)) {
@ -73,6 +78,26 @@ export class AliasController implements IAliasController {
}
}
updateFilters(newFilters: Filters) {
const changedFilterIds: Array<string> = [];
for (const filterId of Object.keys(newFilters)) {
const newFilter = newFilters[filterId];
const prevFilter = this.filters[filterId];
if (!isEqual(newFilter, prevFilter)) {
changedFilterIds.push(filterId);
}
}
for (const filterId of Object.keys(this.filters)) {
if (!newFilters[filterId]) {
changedFilterIds.push(filterId);
}
}
this.filters = deepClone(newFilters);
if (changedFilterIds.length) {
this.filtersChangedSubject.next(changedFilterIds);
}
}
updateAliases(aliasIds?: Array<string>) {
if (!aliasIds) {
aliasIds = [];
@ -116,6 +141,23 @@ export class AliasController implements IAliasController {
return this.entityAliases;
}
getFilters(): Filters {
return this.filters;
}
getFilterInfo(filterId: string): FilterInfo {
return this.filters[filterId];
}
getKeyFilters(filterId: string): Array<KeyFilter> {
const filter = this.getFilterInfo(filterId);
if (filter) {
return filterInfoToKeyFilters(filter);
} else {
return [];
}
}
getEntityAliasId(aliasName: string): string {
for (const aliasId of Object.keys(this.entityAliases)) {
const alias = this.entityAliases[aliasId];
@ -191,6 +233,9 @@ export class AliasController implements IAliasController {
private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> {
const newDatasource = deepClone(datasource);
if (newDatasource.type === DatasourceType.entity) {
if (newDatasource.filterId) {
newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId);
}
if (newDatasource.entityAliasId) {
return this.getAliasInfo(newDatasource.entityAliasId).pipe(
mergeMap((aliasInfo) => {

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

@ -36,7 +36,7 @@ import {
} from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service';
import { EntityDataListener, EntityDataLoadResult } from '@core/api/entity-data.service';
import { deepClone, isDefinedAndNotNull, isObject, objectHashCode } from '@core/utils';
import { deepClone, isDefined, isDefinedAndNotNull, isObject, objectHashCode } from '@core/utils';
import { PageData } from '@shared/models/page/page-data';
import { DataAggregator } from '@core/api/data-aggregator';
import { NULL_UUID } from '@shared/models/id/has-uuid';
@ -497,7 +497,9 @@ export class EntityDataSubscription {
private onDataUpdate(update: Array<EntityData>) {
for (const entityData of update) {
const dataIndex = this.entityIdToDataIndex[entityData.entityId.id];
this.processEntityData(entityData, dataIndex, true, this.notifyListener.bind(this));
if (isDefined(dataIndex) && dataIndex >= 0) {
this.processEntityData(entityData, dataIndex, true, this.notifyListener.bind(this));
}
}
}

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

@ -86,6 +86,13 @@ 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);
if (listener.subscriptionType === widgetType.timeseries) {

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

@ -40,7 +40,14 @@ import { EntityAliases } from '@shared/models/alias.models';
import { EntityInfo } from '@app/shared/models/entity.models';
import { IDashboardComponent } from '@home/models/dashboard-component.models';
import * as moment_ from 'moment';
import { EntityData, EntityDataPageLink, EntityFilter, KeyFilter } from '@shared/models/query/query.models';
import {
EntityData,
EntityDataPageLink,
EntityFilter,
Filter, FilterInfo,
Filters,
KeyFilter
} from '@shared/models/query/query.models';
import { EntityDataService } from '@core/api/entity-data.service';
import { PageData } from '@shared/models/page/page-data';
import { TranslateService } from '@ngx-translate/core';
@ -93,6 +100,7 @@ export interface StateEntityInfo {
export interface IAliasController {
entityAliasesChanged: Observable<Array<string>>;
entityAliasResolved: Observable<string>;
filtersChanged: Observable<Array<string>>;
getAliasInfo(aliasId: string): Observable<AliasInfo>;
getEntityAliasId(aliasName: string): string;
getInstantAliasInfo(aliasId: string): AliasInfo;
@ -100,8 +108,12 @@ export interface IAliasController {
resolveDatasources(datasources: Array<Datasource>, singleEntity?: boolean): Observable<Array<Datasource>>;
resolveAlarmSource(alarmSource: Datasource): Observable<Datasource>;
getEntityAliases(): EntityAliases;
getFilters(): Filters;
getFilterInfo(filterId: string): FilterInfo;
getKeyFilters(filterId: string): Array<KeyFilter>;
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo);
updateEntityAliases(entityAliases: EntityAliases);
updateFilters(filters: Filters);
updateAliases(aliasIds?: Array<string>);
dashboardStateChanged();
}
@ -273,6 +285,8 @@ export interface IWidgetSubscription {
onAliasesChanged(aliasIds: Array<string>): boolean;
onFiltersChanged(filterIds: Array<string>): boolean;
onDashboardTimewindowChanged(dashboardTimewindow: Timewindow): void;
updateDataVisibility(index: number): void;

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

@ -16,7 +16,8 @@
import {
IWidgetSubscription,
SubscriptionEntityInfo, SubscriptionMessage,
SubscriptionEntityInfo,
SubscriptionMessage,
WidgetSubscriptionCallbacks,
WidgetSubscriptionContext,
WidgetSubscriptionOptions
@ -57,7 +58,8 @@ import {
EntityData,
EntityDataPageLink,
entityDataToEntityInfo,
KeyFilter, updateDatasourceFromEntityInfo
KeyFilter,
updateDatasourceFromEntityInfo
} from '@shared/models/query/query.models';
import { map } from 'rxjs/operators';
@ -523,6 +525,17 @@ export class WidgetSubscription implements IWidgetSubscription {
return false;
}
onFiltersChanged(filterIds: Array<string>): boolean {
if (this.type !== widgetType.rpc) {
if (this.type === widgetType.alarm) {
return this.checkAlarmSourceFilters(filterIds);
} else {
return this.checkSubscriptionsFilters(filterIds);
}
}
return false;
}
private onDataUpdated(detectChanges?: boolean) {
if (this.cafs.dataUpdated) {
this.cafs.dataUpdated();
@ -919,6 +932,14 @@ export class WidgetSubscription implements IWidgetSubscription {
}
}
private checkAlarmSourceFilters(filterIds: Array<string>): boolean {
if (this.options.alarmSource && this.options.alarmSource.filterId) {
return filterIds.indexOf(this.options.alarmSource.filterId) > -1;
} else {
return false;
}
}
private checkSubscriptions(aliasIds: Array<string>): boolean {
let subscriptionsChanged = false;
const datasources = this.options.datasources;
@ -939,6 +960,26 @@ export class WidgetSubscription implements IWidgetSubscription {
return subscriptionsChanged;
}
private checkSubscriptionsFilters(filterIds: Array<string>): boolean {
let subscriptionsChanged = false;
const datasources = this.options.datasources;
if (datasources) {
for (const datasource of datasources) {
if (datasource.filterId) {
if (filterIds.indexOf(datasource.filterId) > -1) {
subscriptionsChanged = true;
break;
}
}
}
}
if (subscriptionsChanged && this.hasDataPageLink) {
subscriptionsChanged = false;
this.updateDataSubscriptions();
}
return subscriptionsChanged;
}
private updateDataSubscriptions() {
this.configuredDatasources = this.ctx.utils.validateDatasources(this.options.datasources);
if (!this.ctx.aliasController) {

3
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -129,6 +129,9 @@ export class DashboardUtilsService {
}
dashboard.configuration = this.validateAndUpdateEntityAliases(dashboard.configuration, datasourcesByAliasId, targetDevicesByAliasId);
if (!dashboard.configuration.filters) {
dashboard.configuration.filters = {};
}
if (isUndefined(dashboard.configuration.timewindow)) {
dashboard.configuration.timewindow = this.timeService.defaultTimewindow();

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

@ -26,6 +26,7 @@ import { map } from 'rxjs/operators';
import { FcRuleNode, ruleNodeTypeDescriptors } from '@shared/models/rule-node.models';
import { RuleChainService } from '@core/http/rule-chain.service';
import { RuleChainImport } from '@shared/models/rule-chain.models';
import { Filter, FilterInfo, Filters, FiltersInfo } from '@shared/models/query/query.models';
const WIDGET_ITEM = 'widget_item';
const WIDGET_REFERENCE = 'widget_reference';
@ -35,6 +36,7 @@ const RULE_CHAIN_IMPORT = 'rule_chain_import';
export interface WidgetItem {
widget: Widget;
aliasesInfo: AliasesInfo;
filtersInfo: FiltersInfo;
originalSize: WidgetSize;
originalColumns: number;
}
@ -80,6 +82,9 @@ export class ItemBufferService {
datasourceAliases: {},
targetDeviceAliases: {}
};
const filtersInfo: FiltersInfo = {
datasourceFilters: {}
};
const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout);
const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget);
if (widget.config && dashboard.configuration
@ -108,9 +113,25 @@ export class ItemBufferService {
}
}
}
if (widget.config && dashboard.configuration
&& dashboard.configuration.filters) {
let filter: Filter;
if (widget.config.datasources) {
for (let i = 0; i < widget.config.datasources.length; i++) {
const datasource = widget.config.datasources[i];
if (datasource.type === DatasourceType.entity && datasource.filterId) {
filter = dashboard.configuration.filters[datasource.filterId];
if (filter) {
filtersInfo.datasourceFilters[i] = this.prepareFilterInfo(filter);
}
}
}
}
}
return {
widget,
aliasesInfo,
filtersInfo,
originalSize,
originalColumns
};
@ -145,11 +166,13 @@ export class ItemBufferService {
public pasteWidget(targetDashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId, position: WidgetPosition,
onAliasesUpdateFunction: () => void): Observable<Widget> {
onAliasesUpdateFunction: () => void,
onFiltersUpdateFunction: () => void): Observable<Widget> {
const widgetItem: WidgetItem = this.storeGet(WIDGET_ITEM);
if (widgetItem) {
const widget = widgetItem.widget;
const aliasesInfo = widgetItem.aliasesInfo;
const filtersInfo = widgetItem.filtersInfo;
const originalColumns = widgetItem.originalColumns;
const originalSize = widgetItem.originalSize;
let targetRow = -1;
@ -160,9 +183,9 @@ export class ItemBufferService {
}
widget.id = this.utils.guid();
return this.addWidgetToDashboard(targetDashboard, targetState,
targetLayout, widget, aliasesInfo,
onAliasesUpdateFunction, originalColumns,
originalSize, targetRow, targetColumn).pipe(
targetLayout, widget, aliasesInfo, filtersInfo,
onAliasesUpdateFunction, onFiltersUpdateFunction,
originalColumns, originalSize, targetRow, targetColumn).pipe(
map(() => widget)
);
} else {
@ -186,7 +209,7 @@ export class ItemBufferService {
}
return this.addWidgetToDashboard(targetDashboard, targetState,
targetLayout, widget, null,
null, originalColumns,
null, null, null, originalColumns,
originalSize, targetRow, targetColumn).pipe(
map(() => widget)
);
@ -201,7 +224,9 @@ export class ItemBufferService {
public addWidgetToDashboard(dashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId, widget: Widget,
aliasesInfo: AliasesInfo,
filtersInfo: FiltersInfo,
onAliasesUpdateFunction: () => void,
onFiltersUpdateFunction: () => void,
originalColumns: number,
originalSize: WidgetSize,
row: number,
@ -214,6 +239,7 @@ export class ItemBufferService {
}
theDashboard = this.dashboardUtils.validateAndUpdateDashboard(theDashboard);
let callAliasUpdateFunction = false;
let callFilterUpdateFunction = false;
if (aliasesInfo) {
const newEntityAliases = this.updateAliases(theDashboard, widget, aliasesInfo);
const aliasesUpdated = !isEqual(newEntityAliases, theDashboard.configuration.entityAliases);
@ -224,11 +250,24 @@ export class ItemBufferService {
}
}
}
if (filtersInfo) {
const newFilters = this.updateFilters(theDashboard, widget, filtersInfo);
const filtersUpdated = !isEqual(newFilters, theDashboard.configuration.filters);
if (filtersUpdated) {
theDashboard.configuration.filters = newFilters;
if (onFiltersUpdateFunction) {
callFilterUpdateFunction = true;
}
}
}
this.dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget,
originalColumns, originalSize, row, column);
if (callAliasUpdateFunction) {
onAliasesUpdateFunction();
}
if (callFilterUpdateFunction) {
onFiltersUpdateFunction();
}
return of(theDashboard);
}
@ -368,6 +407,13 @@ export class ItemBufferService {
};
}
private prepareFilterInfo(filter: Filter): FilterInfo {
return {
filter: filter.filter,
keyFilters: filter.keyFilters
};
}
private prepareWidgetReference(dashboard: Dashboard, sourceState: string,
sourceLayout: DashboardLayoutId, widget: Widget): WidgetReference {
const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout);
@ -401,6 +447,19 @@ export class ItemBufferService {
return entityAliases;
}
private updateFilters(dashboard: Dashboard, widget: Widget, filtersInfo: FiltersInfo): Filters {
const filters = deepClone(dashboard.configuration.filters);
let filterInfo: FilterInfo;
let newFilterId: string;
for (const datasourceIndexStr of Object.keys(filtersInfo.datasourceFilters)) {
const datasourceIndex = Number(datasourceIndexStr);
filterInfo = filtersInfo.datasourceFilters[datasourceIndex];
newFilterId = this.getFilterId(filters, filterInfo);
widget.config.datasources[datasourceIndex].filterId = newFilterId;
}
return filters;
}
private isEntityAliasEqual(alias1: EntityAliasInfo, alias2: EntityAliasInfo): boolean {
return isEqual(alias1.filter, alias2.filter);
}
@ -439,6 +498,44 @@ export class ItemBufferService {
return newAlias;
}
private isFilterEqual(filter1: FilterInfo, filter2: FilterInfo): boolean {
return isEqual(filter1.keyFilters, filter2.keyFilters);
}
private getFilterId(filters: Filters, filterInfo: FilterInfo): string {
let newFilterId: string;
for (const filterId of Object.keys(filters)) {
if (this.isFilterEqual(filters[filterId], filterInfo)) {
newFilterId = filterId;
break;
}
}
if (!newFilterId) {
const newFilterName = this.createFilterName(filters, filterInfo.filter);
newFilterId = this.utils.guid();
filters[newFilterId] = {id: newFilterId, filter: newFilterName, keyFilters: filterInfo.keyFilters};
}
return newFilterId;
}
private createFilterName(filters: Filters, filter: string): string {
let c = 0;
let newFilter = filter;
let unique = false;
while (!unique) {
unique = true;
for (const entFilterId of Object.keys(filters)) {
const entFilter = filters[entFilterId];
if (newFilter === entFilter.filter) {
c++;
newFilter = filter + c;
unique = false;
}
}
}
return newFilter;
}
private storeSet(key: string, elem: any) {
localStorage.setItem(this.getNamespacedKey(key), JSON.stringify(elem));
}

3
ui-ngx/src/app/core/services/utils.service.ts

@ -254,6 +254,9 @@ export class UtilsService {
if (datasource.type === DatasourceType.entity && datasource.entityId) {
datasource.name = datasource.entityName;
}
if (!datasource.dataKeys) {
datasource.dataKeys = [];
}
});
return datasources;
}

5
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html

@ -15,8 +15,9 @@
limitations under the License.
-->
<mat-form-field floatLabel="always" hideRequiredMarker [formGroup]="selectEntityAliasFormGroup" class="mat-block">
<mat-label></mat-label>
<mat-form-field [floatLabel]="showLabel ? 'auto' : 'always'"
[hideRequiredMarker]="!showLabel" [formGroup]="selectEntityAliasFormGroup" class="mat-block">
<mat-label *ngIf="!showLabel"></mat-label>
<input matInput type="text" placeholder="{{ 'entity.entity-alias' | translate }}"
#entityAliasInput
formControlName="entityAlias"

3
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts

@ -69,6 +69,9 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
@Input()
callbacks: EntityAliasSelectCallbacks;
@Input()
showLabel: boolean;
@ViewChild('entityAliasAutocomplete') entityAliasAutocomplete: MatAutocomplete;
@ViewChild('autocomplete', { read: MatAutocompleteTrigger }) autoCompleteTrigger: MatAutocompleteTrigger;

6
ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.ts

@ -39,6 +39,7 @@ import { mergeMap } from 'rxjs/operators';
import { AliasesInfo } from '@shared/models/alias.models';
import { ItemBufferService } from '@core/services/item-buffer.service';
import { StateObject } from '@core/api/widget-api.models';
import { FiltersInfo } from '@shared/models/query/query.models';
export interface AddWidgetToDashboardDialogData {
entityId: EntityId;
@ -182,8 +183,11 @@ export class AddWidgetToDashboardDialogComponent extends
alias: this.data.entityName,
filter: this.dashboardUtils.createSingleEntityFilter(this.data.entityId)
};
const filtersInfo: FiltersInfo = {
datasourceFilters: {}
};
this.itembuffer.addWidgetToDashboard(dashboard, targetState,
targetLayout, this.data.widget, aliasesInfo, null,
targetLayout, this.data.widget, aliasesInfo, filtersInfo, null, null,
48, null, -1, -1).pipe(
mergeMap((theDashboard) => {
return this.dashboardService.saveDashboard(theDashboard);

5
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts

@ -80,6 +80,7 @@ import {
AddWidgetToDashboardDialogData
} from '@home/components/attribute/add-widget-to-dashboard-dialog.component';
import { deepClone } from '@core/utils';
import { Filters } from '@shared/models/query/query.models';
@Component({
@ -390,9 +391,11 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
}
};
const filters: Filters = {};
this.aliasController = new AliasController(this.utils,
this.entityService,
() => stateController, entitiAliases);
() => stateController, entitiAliases, filters);
const dataKeyType: DataKeyType = this.attributeScope === LatestTelemetry.LATEST_TELEMETRY ?
DataKeyType.timeseries : DataKeyType.attribute;

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

@ -0,0 +1,30 @@
<!--
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="row" [formGroup]="booleanFilterPredicateFormGroup">
<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 booleanOperations" [value]="operation">
{{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox formControlName="value">
{{ 'filter.value' | translate }}
</mat-checkbox>
</div>

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

@ -0,0 +1,101 @@
///
/// 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, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
BooleanFilterPredicate,
BooleanOperation, booleanOperationTranslationMap,
FilterPredicateType
} from '@shared/models/query/query.models';
import { isDefined } from '@core/utils';
@Component({
selector: 'tb-boolean-filter-predicate',
templateUrl: './boolean-filter-predicate.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BooleanFilterPredicateComponent),
multi: true
}
]
})
export class BooleanFilterPredicateComponent implements ControlValueAccessor, OnInit {
@Input() disabled: boolean;
@Input() userMode: boolean;
booleanFilterPredicateFormGroup: FormGroup;
booleanOperations = Object.keys(BooleanOperation);
booleanOperationEnum = BooleanOperation;
booleanOperationTranslations = booleanOperationTranslationMap;
private propagateChange = null;
constructor(private fb: FormBuilder) {
}
ngOnInit(): void {
this.booleanFilterPredicateFormGroup = this.fb.group({
operation: [BooleanOperation.EQUAL, [Validators.required]],
value: [false]
});
if (this.userMode) {
this.booleanFilterPredicateFormGroup.get('operation').disable({emitEvent: false});
}
this.booleanFilterPredicateFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.booleanFilterPredicateFormGroup.disable({emitEvent: false});
} else {
this.booleanFilterPredicateFormGroup.enable({emitEvent: false});
}
}
writeValue(predicate: BooleanFilterPredicate): void {
this.booleanFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false});
this.booleanFilterPredicateFormGroup.get('value').patchValue(isDefined(predicate.value) ? predicate.value : false, {emitEvent: false});
}
private updateModel() {
let predicate: BooleanFilterPredicate = null;
if (this.booleanFilterPredicateFormGroup.valid) {
predicate = this.booleanFilterPredicateFormGroup.getRawValue();
if (!isDefined(predicate.value)) {
predicate.value = false;
}
predicate.type = FilterPredicateType.BOOLEAN;
}
this.propagateChange(predicate);
}
}

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

@ -0,0 +1,59 @@
<!--
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.
-->
<form [formGroup]="complexFilterFormGroup" (ngSubmit)="save()">
<mat-toolbar color="primary">
<h2 translate>filter.complex-filter</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" fxLayout="column">
<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 complexOperations" [value]="operation">
{{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<tb-filter-predicate-list
[userMode]="data.userMode"
[valueType]="data.valueType"
formControlName="predicates">
</tb-filter-predicate-list>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid">
{{ 'action.update' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()"
cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

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

@ -0,0 +1,94 @@
///
/// 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, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
BooleanOperation, booleanOperationTranslationMap,
ComplexFilterPredicate, ComplexOperation, complexOperationTranslationMap,
EntityKeyValueType,
FilterPredicateType
} from '@shared/models/query/query.models';
export interface ComplexFilterPredicateDialogData {
complexPredicate: ComplexFilterPredicate;
userMode: boolean;
disabled: boolean;
valueType: EntityKeyValueType;
}
@Component({
selector: 'tb-complex-filter-predicate-dialog',
templateUrl: './complex-filter-predicate-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: ComplexFilterPredicateDialogComponent}],
styleUrls: []
})
export class ComplexFilterPredicateDialogComponent extends
DialogComponent<ComplexFilterPredicateDialogComponent, ComplexFilterPredicate>
implements OnInit, ErrorStateMatcher {
complexFilterFormGroup: FormGroup;
complexOperations = Object.keys(ComplexOperation);
complexOperationEnum = ComplexOperation;
complexOperationTranslations = complexOperationTranslationMap;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ComplexFilterPredicateDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<ComplexFilterPredicateDialogComponent, ComplexFilterPredicate>,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.complexFilterFormGroup = this.fb.group(
{
operation: [this.data.complexPredicate.operation, [Validators.required]],
predicates: [this.data.complexPredicate.predicates, [Validators.required]]
}
);
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
if (this.complexFilterFormGroup.valid) {
const predicate: ComplexFilterPredicate = this.complexFilterFormGroup.getRawValue();
predicate.type = FilterPredicateType.COMPLEX;
this.dialogRef.close(predicate);
}
}
}

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

@ -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.
-->
<div fxLayout="row">
<mat-label translate>filter.complex-filter</mat-label>
<button mat-icon-button color="primary"
[fxShow]="!disabled"
type="button"
(click)="openComplexFilterDialog()"
matTooltip="{{ 'filter.edit-complex-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
</div>

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

@ -0,0 +1,97 @@
///
/// 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, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ComplexFilterPredicate, EntityKeyValueType } from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog';
import {
ComplexFilterPredicateDialogComponent,
ComplexFilterPredicateDialogData
} from '@home/components/filter/complex-filter-predicate-dialog.component';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-complex-filter-predicate',
templateUrl: './complex-filter-predicate.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ComplexFilterPredicateComponent),
multi: true
}
]
})
export class ComplexFilterPredicateComponent implements ControlValueAccessor, OnInit {
@Input() disabled: boolean;
@Input() userMode: boolean;
@Input() valueType: EntityKeyValueType;
private propagateChange = null;
private complexFilterPredicate: ComplexFilterPredicate;
constructor(private dialog: MatDialog) {
}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(predicate: ComplexFilterPredicate): void {
this.complexFilterPredicate = predicate;
}
private openComplexFilterDialog() {
this.dialog.open<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateDialogData,
ComplexFilterPredicate>(ComplexFilterPredicateDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
complexPredicate: deepClone(this.complexFilterPredicate),
disabled: this.disabled,
userMode: this.userMode,
valueType: this.valueType
}
}).afterClosed().subscribe(
(result) => {
if (result) {
this.complexFilterPredicate = result;
this.updateModel();
}
}
);
}
private updateModel() {
this.propagateChange(this.complexFilterPredicate);
}
}

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

@ -0,0 +1,63 @@
<!--
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.
-->
<form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="min-width: 480px;">
<mat-toolbar color="primary">
<h2>{{ (isAdd ? 'filter.add' : 'filter.edit') | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<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>
<tb-key-filter-list
formControlName="keyFilters"
[userMode]="userMode">
</tb-key-filter-list>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

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

@ -0,0 +1,137 @@
///
/// 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, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NgForm,
ValidatorFn,
Validators
} from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { Filter, Filters } from '@shared/models/query/query.models';
export interface FilterDialogData {
isAdd: boolean;
userMode: boolean;
filters: Filters | Array<Filter>;
filter?: Filter;
}
@Component({
selector: 'tb-filter-dialog',
templateUrl: './filter-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: FilterDialogComponent}],
styleUrls: []
})
export class FilterDialogComponent extends DialogComponent<FilterDialogComponent, Filter>
implements OnInit, ErrorStateMatcher {
isAdd: boolean;
userMode: boolean;
filters: Array<Filter>;
filter: Filter;
filterFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: FilterDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<FilterDialogComponent, Filter>,
private fb: FormBuilder,
private utils: UtilsService,
public translate: TranslateService) {
super(store, router, dialogRef);
this.isAdd = data.isAdd;
this.userMode = data.userMode;
if (Array.isArray(data.filters)) {
this.filters = data.filters;
} else {
this.filters = [];
for (const filterId of Object.keys(data.filters)) {
this.filters.push(data.filters[filterId]);
}
}
if (this.isAdd && !this.data.filter) {
this.filter = {
id: null,
filter: '',
keyFilters: []
};
} else {
this.filter = data.filter;
}
this.filterFormGroup = this.fb.group({
filter: [this.filter.filter, [this.validateDuplicateFilterName(), Validators.required]],
keyFilters: [this.filter.keyFilters, Validators.required]
});
}
validateDuplicateFilterName(): ValidatorFn {
return (c: FormControl) => {
const newFilter = c.value;
const found = this.filters.find((filter) => filter.filter === newFilter);
if (found) {
if (this.isAdd || this.filter.id !== found.id) {
return {
duplicateFilterName: {
valid: false
}
};
}
}
return null;
};
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
this.filter.filter = this.filterFormGroup.get('filter').value;
this.filter.keyFilters = this.filterFormGroup.get('keyFilters').value;
if (this.isAdd) {
this.filter.id = this.utils.guid();
}
this.dialogRef.close(this.filter);
}
}

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

@ -0,0 +1,57 @@
<!--
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 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>
</section>

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

@ -0,0 +1,159 @@
///
/// 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, forwardRef, Input, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormArray,
FormBuilder,
FormGroup,
NG_VALUE_ACCESSOR,
Validators
} from '@angular/forms';
import { Observable, of, Subscription } from 'rxjs';
import {
ComplexFilterPredicate,
createDefaultFilterPredicate,
EntityKeyValueType,
KeyFilterPredicate
} from '@shared/models/query/query.models';
import {
ComplexFilterPredicateDialogComponent,
ComplexFilterPredicateDialogData
} from '@home/components/filter/complex-filter-predicate-dialog.component';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'tb-filter-predicate-list',
templateUrl: './filter-predicate-list.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FilterPredicateListComponent),
multi: true
}
]
})
export class FilterPredicateListComponent implements ControlValueAccessor, OnInit {
@Input() disabled: boolean;
@Input() userMode: boolean;
@Input() valueType: EntityKeyValueType;
filterListFormGroup: FormGroup;
private propagateChange = null;
private valueChangeSubscription: Subscription = null;
constructor(private fb: FormBuilder,
private dialog: MatDialog) {
}
ngOnInit(): void {
this.filterListFormGroup = this.fb.group({});
this.filterListFormGroup.addControl('predicates',
this.fb.array([]));
}
predicatesFormArray(): FormArray {
return this.filterListFormGroup.get('predicates') as FormArray;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.filterListFormGroup.disable({emitEvent: false});
} else {
this.filterListFormGroup.enable({emitEvent: false});
}
}
writeValue(predicates: Array<KeyFilterPredicate>): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const predicateControls: Array<AbstractControl> = [];
if (predicates) {
for (const predicate of predicates) {
predicateControls.push(this.fb.control(predicate, [Validators.required]));
}
}
this.filterListFormGroup.setControl('predicates', this.fb.array(predicateControls));
this.valueChangeSubscription = this.filterListFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
if (this.disabled) {
this.filterListFormGroup.disable({emitEvent: false});
} else {
this.filterListFormGroup.enable({emitEvent: false});
}
}
public removePredicate(index: number) {
(this.filterListFormGroup.get('predicates') as FormArray).removeAt(index);
}
public addPredicate(complex: boolean) {
const predicatesFormArray = this.filterListFormGroup.get('predicates') as FormArray;
const predicate = createDefaultFilterPredicate(this.valueType, complex);
let observable: Observable<KeyFilterPredicate>;
if (complex) {
observable = this.openComplexFilterDialog(predicate as ComplexFilterPredicate);
} else {
observable = of(predicate);
}
observable.subscribe((result) => {
if (result) {
predicatesFormArray.push(this.fb.control(result, [Validators.required]));
}
});
}
private openComplexFilterDialog(predicate: ComplexFilterPredicate): Observable<KeyFilterPredicate> {
return this.dialog.open<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateDialogData,
ComplexFilterPredicate>(ComplexFilterPredicateDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
complexPredicate: predicate,
disabled: this.disabled,
userMode: this.userMode,
valueType: this.valueType
}
}).afterClosed();
}
private updateModel() {
const predicates: Array<KeyFilterPredicate> = this.filterListFormGroup.getRawValue().predicates;
if (predicates.length) {
this.propagateChange(predicates);
} else {
this.propagateChange(null);
}
}
}

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

@ -0,0 +1,42 @@
<!--
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" [formGroup]="filterPredicateFormGroup"
[ngSwitch]="type">
<ng-template [ngSwitchCase]="filterPredicateType.STRING">
<tb-string-filter-predicate [userMode]="userMode"
formControlName="predicate">
</tb-string-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.NUMERIC">
<tb-numeric-filter-predicate [userMode]="userMode"
formControlName="predicate">
</tb-numeric-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.BOOLEAN">
<tb-boolean-filter-predicate [userMode]="userMode"
formControlName="predicate">
</tb-boolean-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.COMPLEX">
<tb-complex-filter-predicate
[valueType]="valueType"
[userMode]="userMode"
formControlName="predicate">
</tb-complex-filter-predicate>
</ng-template>
</div>

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

@ -0,0 +1,93 @@
///
/// 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, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
EntityKeyValueType,
FilterPredicateType, KeyFilterPredicate
} from '@shared/models/query/query.models';
@Component({
selector: 'tb-filter-predicate',
templateUrl: './filter-predicate.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FilterPredicateComponent),
multi: true
}
]
})
export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
@Input() disabled: boolean;
@Input() userMode: boolean;
@Input() valueType: EntityKeyValueType;
filterPredicateFormGroup: FormGroup;
type: FilterPredicateType;
filterPredicateType = FilterPredicateType;
private propagateChange = null;
constructor(private fb: FormBuilder) {
}
ngOnInit(): void {
this.filterPredicateFormGroup = this.fb.group({
predicate: [null, [Validators.required]]
});
this.filterPredicateFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.filterPredicateFormGroup.disable({emitEvent: false});
} else {
this.filterPredicateFormGroup.enable({emitEvent: false});
}
}
writeValue(predicate: KeyFilterPredicate): void {
this.type = predicate.type;
this.filterPredicateFormGroup.get('predicate').patchValue(predicate, {emitEvent: false});
}
private updateModel() {
let predicate: KeyFilterPredicate = null;
if (this.filterPredicateFormGroup.valid) {
predicate = this.filterPredicateFormGroup.getRawValue().predicate;
}
this.propagateChange(predicate);
}
}

61
ui-ngx/src/app/modules/home/components/filter/filter-select.component.html

@ -0,0 +1,61 @@
<!--
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.
-->
<mat-form-field [floatLabel]="showLabel ? 'auto' : 'always'"
[hideRequiredMarker]="!showLabel" [formGroup]="selectFilterFormGroup" class="mat-block">
<mat-label *ngIf="!showLabel"></mat-label>
<input matInput type="text" placeholder="{{ 'filter.filter' | translate }}"
#filterInput
formControlName="filter"
(focusin)="onFocus()"
[required]="tbRequired"
(keydown)="filterEnter($event)"
(keypress)="filterEnter($event)"
[matAutocomplete]="filterAutocomplete">
<button *ngIf="selectFilterFormGroup.get('filter').value && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete class="tb-autocomplete"
#filterAutocomplete="matAutocomplete"
[displayWith]="displayFilterFn">
<mat-option *ngFor="let filter of filteredFilters | async" [value]="filter">
<span [innerHTML]="filter.filter | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredFilters | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
<span translate>filter.no-filters-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('filter.no-filter-matching',
{filter: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span>
</ng-template>
<span>
<a translate (click)="createFilter($event, searchText)">filter.create-new-filter</a>
</span>
</div>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="!modelValue && tbRequired">
{{ 'filter.filter-required' | translate }}
</mat-error>
</mat-form-field>

22
ui-ngx/src/app/modules/home/components/filter/filter-select.component.models.ts

@ -0,0 +1,22 @@
///
/// 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 { Observable } from 'rxjs';
import { Filter } from '@shared/models/query/query.models';
export interface FilterSelectCallbacks {
createFilter: (filter: string) => Observable<Filter>;
}

24
ui-ngx/src/app/modules/home/components/filter/filter-select.component.scss

@ -0,0 +1,24 @@
/**
* 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 {
}
:host ::ng-deep {
.mat-form-field-infix {
border-top: none;
}
}

249
ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts

@ -0,0 +1,249 @@
///
/// 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 { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NG_VALUE_ACCESSOR,
NgForm
} from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, mergeMap, share, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { IAliasController } from '@core/api/widget-api.models';
import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { ENTER } from '@angular/cdk/keycodes';
import { ErrorStateMatcher } from '@angular/material/core';
import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models';
import { Filter } from '@shared/models/query/query.models';
@Component({
selector: 'tb-filter-select',
templateUrl: './filter-select.component.html',
styleUrls: ['./filter-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FilterSelectComponent),
multi: true
},
{
provide: ErrorStateMatcher,
useExisting: FilterSelectComponent
}]
})
export class FilterSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher {
selectFilterFormGroup: FormGroup;
modelValue: string | null;
@Input()
aliasController: IAliasController;
@Input()
callbacks: FilterSelectCallbacks;
@Input()
showLabel: boolean;
@ViewChild('filterAutocomplete') filterAutocomplete: MatAutocomplete;
@ViewChild('autocomplete', { read: MatAutocompleteTrigger }) autoCompleteTrigger: MatAutocompleteTrigger;
private requiredValue: boolean;
get tbRequired(): boolean {
return this.requiredValue;
}
@Input()
set tbRequired(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
@ViewChild('filterInput', {static: true}) filterInput: ElementRef;
filterList: Array<Filter> = [];
filteredFilters: Observable<Array<Filter>>;
searchText = '';
private dirty = false;
private creatingFilter = false;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public translate: TranslateService,
public truncate: TruncatePipe,
private fb: FormBuilder) {
this.selectFilterFormGroup = this.fb.group({
filter: [null]
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
const filters = this.aliasController.getFilters();
for (const filterId of Object.keys(filters)) {
this.filterList.push(filters[filterId]);
}
this.filteredFilters = this.selectFilterFormGroup.get('filter').valueChanges
.pipe(
tap(value => {
let modelValue;
if (typeof value === 'string' || !value) {
modelValue = null;
} else {
modelValue = value;
}
this.updateView(modelValue);
if (value === null) {
this.clear();
}
}),
map(value => value ? (typeof value === 'string' ? value : value.filter) : ''),
mergeMap(name => this.fetchFilters(name) ),
share()
);
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = this.tbRequired && !this.modelValue;
return originalErrorState || customErrorState;
}
ngAfterViewInit(): void {}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.selectFilterFormGroup.disable({emitEvent: false});
} else {
this.selectFilterFormGroup.enable({emitEvent: false});
}
}
writeValue(value: string | null): void {
this.searchText = '';
let filter = null;
if (value != null) {
const filters = this.aliasController.getFilters();
if (filters[value]) {
filter = filters[value];
}
}
if (filter != null) {
this.modelValue = filter.id;
this.selectFilterFormGroup.get('filter').patchValue(filter, {emitEvent: false});
} else {
this.modelValue = null;
this.selectFilterFormGroup.get('filter').patchValue('', {emitEvent: false});
}
this.dirty = true;
}
onFocus() {
if (this.dirty) {
this.selectFilterFormGroup.get('filter').updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
updateView(value: Filter | null) {
const filterId = value ? value.id : null;
if (this.modelValue !== filterId) {
this.modelValue = filterId;
this.propagateChange(this.modelValue);
}
}
displayFilterFn(filter?: Filter): string | undefined {
return filter ? filter.filter : undefined;
}
fetchFilters(searchText?: string): Observable<Array<Filter>> {
this.searchText = searchText;
let result = this.filterList;
if (searchText && searchText.length) {
result = this.filterList.filter((filter) => filter.filter.toLowerCase().includes(searchText.toLowerCase()));
}
return of(result);
}
clear(value: string = '') {
this.filterInput.nativeElement.value = value;
this.selectFilterFormGroup.get('filter').patchValue(value, {emitEvent: true});
setTimeout(() => {
this.filterInput.nativeElement.blur();
this.filterInput.nativeElement.focus();
}, 0);
}
textIsNotEmpty(text: string): boolean {
return (text && text != null && text.length > 0) ? true : false;
}
filterEnter($event: KeyboardEvent) {
if ($event.keyCode === ENTER) {
$event.preventDefault();
if (!this.modelValue) {
this.createFilter($event, this.searchText);
}
}
}
createFilter($event: Event, filter: string) {
$event.preventDefault();
this.creatingFilter = true;
if (this.callbacks && this.callbacks.createFilter) {
this.callbacks.createFilter(filter).subscribe((newFilter) => {
if (!newFilter) {
setTimeout(() => {
this.filterInput.nativeElement.blur();
this.filterInput.nativeElement.focus();
}, 0);
} else {
this.filterList.push(newFilter);
this.modelValue = newFilter.id;
this.selectFilterFormGroup.get('filter').patchValue(newFilter, {emitEvent: true});
this.propagateChange(this.modelValue);
}
}
);
}
}
}

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

@ -0,0 +1,99 @@
<!--
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.
-->
<form [formGroup]="filtersFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary">
<h2>{{ title | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<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>
</div>
<fieldset [disabled]="isLoading$ | async">
<mat-divider></mat-divider>
<div fxFlex fxLayout="row" fxLayoutAlign="start center"
formArrayName="filters"
*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>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="editFilter($index)"
matTooltip="{{ 'filter.edit' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="removeFilter($index)"
matTooltip="{{ 'filter.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</fieldset>
</div>
<div mat-dialog-actions>
<button mat-raised-button color="primary"
type="button"
(click)="addFilter()"
[fxShow]="!disableAdd"
[disabled]="isLoading$ | async"
matTooltip="{{ 'filter.add' | translate }}"
matTooltipPosition="above">
{{ 'filter.add' | translate }}
</button>
<span fxFlex></span>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || filtersFormGroup.invalid || !filtersFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()"
cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

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

@ -0,0 +1,44 @@
/**
* 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-filters-header {
min-height: 40px;
padding: 0 11px;
margin: 5px;
.tb-header-label {
font-size: 14px;
color: rgba(0, 0, 0, .570588);
}
}
mat-divider{
margin: -1px -24px;
}
.tb-filter {
padding: 0 0 0 10px;
margin: 5px;
}
}
:host ::ng-deep {
.mat-dialog-content {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
}

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

@ -0,0 +1,241 @@
///
/// 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, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
AbstractControl,
FormArray,
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NgForm,
Validators
} from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { DatasourceType, Widget } from '@shared/models/widget.models';
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 { Filter, Filters, KeyFilterInfo } from '@shared/models/query/query.models';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
export interface FiltersDialogData {
filters: Filters;
widgets: Array<Widget>;
isSingleFilter?: boolean;
isSingleWidget?: boolean;
disableAdd?: boolean;
singleFilter?: Filter;
customTitle?: string;
}
@Component({
selector: 'tb-filters-dialog',
templateUrl: './filters-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: FiltersDialogComponent}],
styleUrls: ['./filters-dialog.component.scss']
})
export class FiltersDialogComponent extends DialogComponent<FiltersDialogComponent, Filters>
implements OnInit, ErrorStateMatcher {
title: string;
disableAdd: boolean;
filterToWidgetsMap: {[filterId: string]: Array<string>} = {};
filtersFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: FiltersDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<FiltersDialogComponent, Filters>,
private fb: FormBuilder,
private utils: UtilsService,
private translate: TranslateService,
private dialogs: DialogService,
private dialog: MatDialog) {
super(store, router, dialogRef);
this.title = data.customTitle ? data.customTitle : 'filter.filters';
this.disableAdd = this.data.disableAdd;
if (data.widgets) {
let widgetsTitleList: Array<string>;
if (this.data.isSingleWidget && this.data.widgets.length === 1) {
const widget = this.data.widgets[0];
widgetsTitleList = [widget.config.title];
for (const filterId of Object.keys(this.data.filters)) {
this.filterToWidgetsMap[filterId] = widgetsTitleList;
}
} else {
this.data.widgets.forEach((widget) => {
const datasources = this.utils.validateDatasources(widget.config.datasources);
datasources.forEach((datasource) => {
if (datasource.type === DatasourceType.entity && datasource.filterId) {
widgetsTitleList = this.filterToWidgetsMap[datasource.filterId];
if (!widgetsTitleList) {
widgetsTitleList = [];
this.filterToWidgetsMap[datasource.filterId] = widgetsTitleList;
}
widgetsTitleList.push(widget.config.title);
}
});
});
}
}
const filterControls: Array<AbstractControl> = [];
for (const filterId of Object.keys(this.data.filters)) {
const filter = this.data.filters[filterId];
filterControls.push(this.createFilterFormControl(filterId, filter));
}
this.filtersFormGroup = this.fb.group({
filters: this.fb.array(filterControls)
});
}
private createFilterFormControl(filterId: string, filter: Filter): AbstractControl {
const filterFormControl = this.fb.group({
id: [filterId],
filter: [filter ? filter.filter : null, [Validators.required]],
keyFilters: [filter ? filter.keyFilters : [], [Validators.required]]
});
return filterFormControl;
}
filtersFormArray(): FormArray {
return this.filtersFormGroup.get('filters') as FormArray;
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
removeFilter(index: number) {
const filter = (this.filtersFormGroup.get('filters').value as any[])[index];
const widgetsTitleList = this.filterToWidgetsMap[filter.id];
if (widgetsTitleList) {
let widgetsListHtml = '';
for (const widgetTitle of widgetsTitleList) {
widgetsListHtml += '<br/>\'' + widgetTitle + '\'';
}
const message = this.translate.instant('entity.unable-delete-filter-text',
{filter: filter.filter, widgetsList: widgetsListHtml});
this.dialogs.alert(this.translate.instant('entity.unable-delete-filter-title'),
message, this.translate.instant('action.close'), true);
} else {
(this.filtersFormGroup.get('filters') as FormArray).removeAt(index);
this.filtersFormGroup.markAsDirty();
}
}
public addFilter() {
this.openFilterDialog(-1);
}
public editFilter(index: number) {
this.openFilterDialog(index);
}
private openFilterDialog(index: number) {
const isAdd = index === -1;
let filter;
const filtersArray = this.filtersFormGroup.get('filters').value as any[];
if (!isAdd) {
filter = filtersArray[index];
}
this.dialog.open<FilterDialogComponent, FilterDialogData,
Filter>(FilterDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd,
filters: filtersArray,
filter: isAdd ? null : deepClone(filter),
userMode: false
}
}).afterClosed().subscribe((result) => {
if (result) {
if (isAdd) {
(this.filtersFormGroup.get('filters') as FormArray)
.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);
}
this.filtersFormGroup.markAsDirty();
}
});
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
const filters: Filters = {};
const uniqueFilterList: {[filter: string]: string} = {};
let valid = true;
let message: string;
const filtersArray = this.filtersFormGroup.get('filters').value as any[];
for (const filterValue of filtersArray) {
const filterId: string = filterValue.id;
const filter: string = filterValue.filter;
const keyFilters: Array<KeyFilterInfo> = filterValue.keyFilters;
if (uniqueFilterList[filter]) {
valid = false;
message = this.translate.instant('filter.duplicate-filter-error', {filter});
break;
} else if (!keyFilters || !keyFilters.length) {
valid = false;
message = this.translate.instant('filter.missing-key-filters-error', {filter});
break;
} else {
uniqueFilterList[filter] = filter;
filters[filterId] = {id: filterId, filter, keyFilters};
}
}
if (valid) {
this.dialogRef.close(filters);
} else {
this.store.dispatch(new ActionNotificationShow(
{
message,
type: 'error'
}));
}
}
}

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

@ -0,0 +1,86 @@
<!--
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.
-->
<form [formGroup]="keyFilterFormGroup" (ngSubmit)="save()">
<mat-toolbar color="primary">
<h2 translate>{{data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter'}}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</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">
<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-label translate>filter.key-type.key-type</mat-label>
<mat-select required formControlName="type">
<mat-option *ngFor="let type of entityKeyTypes" [value]="type">
{{entityKeyTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
</section>
<mat-form-field fxFlex="40" 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>
<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>
<span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span>
</mat-option>
</mat-select>
<mat-error *ngIf="keyFilterFormGroup.get('valueType').hasError('required')">
{{ 'filter.value-type-required' | translate }}
</mat-error>
</mat-form-field>
</section>
<tb-filter-predicate-list *ngIf="keyFilterFormGroup.get('valueType').value"
[userMode]="data.userMode"
[valueType]="keyFilterFormGroup.get('valueType').value"
formControlName="predicates">
</tb-filter-predicate-list>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()"
cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

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

@ -0,0 +1,105 @@
///
/// 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, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
EntityKeyType,
entityKeyTypeTranslationMap,
EntityKeyValueType,
entityKeyValueTypesMap,
KeyFilterInfo
} from '@shared/models/query/query.models';
export interface KeyFilterDialogData {
keyFilter: KeyFilterInfo;
userMode: boolean;
isAdd: boolean;
}
@Component({
selector: 'tb-key-filter-dialog',
templateUrl: './key-filter-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: KeyFilterDialogComponent}],
styleUrls: []
})
export class KeyFilterDialogComponent extends
DialogComponent<KeyFilterDialogComponent, KeyFilterInfo>
implements OnInit, ErrorStateMatcher {
keyFilterFormGroup: FormGroup;
entityKeyTypes = [EntityKeyType.ENTITY_FIELD, EntityKeyType.ATTRIBUTE, EntityKeyType.TIME_SERIES];
entityKeyTypeTranslations = entityKeyTypeTranslationMap;
entityKeyValueTypesKeys = Object.keys(EntityKeyValueType);
entityKeyValueTypeEnum = EntityKeyValueType;
entityKeyValueTypes = entityKeyValueTypesMap;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: KeyFilterDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<KeyFilterDialogComponent, KeyFilterInfo>,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.keyFilterFormGroup = this.fb.group(
{
key: this.fb.group(
{
type: [this.data.keyFilter.key.type, [Validators.required]],
key: [this.data.keyFilter.key.key, [Validators.required]]
}
),
valueType: [this.data.keyFilter.valueType, [Validators.required]],
predicates: [this.data.keyFilter.predicates, [Validators.required]]
}
);
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
if (this.keyFilterFormGroup.valid) {
const keyFilter: KeyFilterInfo = this.keyFilterFormGroup.getRawValue();
this.dialogRef.close(keyFilter);
}
}
}

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

@ -0,0 +1,53 @@
<!--
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 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>
</section>

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

@ -0,0 +1,165 @@
///
/// 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, forwardRef, Input, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormArray,
FormBuilder,
FormGroup,
NG_VALUE_ACCESSOR,
Validators
} from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { EntityKeyType, 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';
@Component({
selector: 'tb-key-filter-list',
templateUrl: './key-filter-list.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => KeyFilterListComponent),
multi: true
}
]
})
export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
@Input() disabled: boolean;
@Input() userMode: boolean;
keyFilterListFormGroup: FormGroup;
private propagateChange = null;
private valueChangeSubscription: Subscription = null;
constructor(private fb: FormBuilder,
private dialog: MatDialog) {
}
ngOnInit(): void {
this.keyFilterListFormGroup = this.fb.group({});
this.keyFilterListFormGroup.addControl('keyFilters',
this.fb.array([]));
}
keyFiltersFormArray(): FormArray {
return this.keyFilterListFormGroup.get('keyFilters') as FormArray;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.keyFilterListFormGroup.disable({emitEvent: false});
} else {
this.keyFilterListFormGroup.enable({emitEvent: false});
}
}
writeValue(keyFilters: Array<KeyFilterInfo>): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const keyFilterControls: Array<AbstractControl> = [];
if (keyFilters) {
for (const keyFilter of keyFilters) {
keyFilterControls.push(this.fb.control(keyFilter, [Validators.required]));
}
}
this.keyFilterListFormGroup.setControl('keyFilters', this.fb.array(keyFilterControls));
this.valueChangeSubscription = this.keyFilterListFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
if (this.disabled) {
this.keyFilterListFormGroup.disable({emitEvent: false});
} else {
this.keyFilterListFormGroup.enable({emitEvent: false});
}
}
public removeKeyFilter(index: number) {
(this.keyFilterListFormGroup.get('keyFilters') as FormArray).removeAt(index);
}
public addKeyFilter() {
const keyFiltersFormArray = this.keyFilterListFormGroup.get('keyFilters') as FormArray;
this.openKeyFilterDialog(null).subscribe((result) => {
if (result) {
keyFiltersFormArray.push(this.fb.control(result, [Validators.required]));
}
});
}
public editKeyFilter(index: number) {
const keyFilter: KeyFilterInfo =
(this.keyFilterListFormGroup.get('keyFilters') as FormArray).at(index).value;
this.openKeyFilterDialog(keyFilter).subscribe(
(result) => {
if (result) {
(this.keyFilterListFormGroup.get('keyFilters') as FormArray).at(index).patchValue(result);
}
}
);
}
private openKeyFilterDialog(keyFilter?: KeyFilterInfo): Observable<KeyFilterInfo> {
const isAdd = !keyFilter;
if (!keyFilter) {
keyFilter = {
key: {
key: '',
type: EntityKeyType.ATTRIBUTE
},
valueType: null,
predicates: []
};
}
return this.dialog.open<KeyFilterDialogComponent, KeyFilterDialogData,
KeyFilterInfo>(KeyFilterDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
keyFilter: keyFilter ? deepClone(keyFilter): null,
userMode: this.userMode,
isAdd
}
}).afterClosed();
}
private updateModel() {
const keyFilters: Array<KeyFilterInfo> = this.keyFilterListFormGroup.getRawValue().keyFilters;
if (keyFilters.length) {
this.propagateChange(keyFilters);
} else {
this.propagateChange(null);
}
}
}

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

@ -0,0 +1,31 @@
<!--
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="row" [formGroup]="numericFilterPredicateFormGroup">
<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 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>
</div>

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

@ -0,0 +1,99 @@
///
/// 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, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
FilterPredicateType, NumericFilterPredicate, NumericOperation, numericOperationTranslationMap,
} from '@shared/models/query/query.models';
import { isDefined } from '@core/utils';
@Component({
selector: 'tb-numeric-filter-predicate',
templateUrl: './numeric-filter-predicate.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NumericFilterPredicateComponent),
multi: true
}
]
})
export class NumericFilterPredicateComponent implements ControlValueAccessor, OnInit {
@Input() disabled: boolean;
@Input() userMode: boolean;
numericFilterPredicateFormGroup: FormGroup;
numericOperations = Object.keys(NumericOperation);
numericOperationEnum = NumericOperation;
numericOperationTranslations = numericOperationTranslationMap;
private propagateChange = null;
constructor(private fb: FormBuilder) {
}
ngOnInit(): void {
this.numericFilterPredicateFormGroup = this.fb.group({
operation: [NumericOperation.EQUAL, [Validators.required]],
value: [0, [Validators.required]]
});
if (this.userMode) {
this.numericFilterPredicateFormGroup.get('operation').disable({emitEvent: false});
}
this.numericFilterPredicateFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.numericFilterPredicateFormGroup.disable({emitEvent: false});
} else {
this.numericFilterPredicateFormGroup.enable({emitEvent: false});
}
}
writeValue(predicate: NumericFilterPredicate): void {
this.numericFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false});
this.numericFilterPredicateFormGroup.get('value').patchValue(isDefined(predicate.value) ? predicate.value : 0, {emitEvent: false});
}
private updateModel() {
let predicate: NumericFilterPredicate = null;
if (this.numericFilterPredicateFormGroup.valid) {
predicate = this.numericFilterPredicateFormGroup.getRawValue();
if (!predicate.value) {
predicate.value = 0;
}
predicate.type = FilterPredicateType.NUMERIC;
}
this.propagateChange(predicate);
}
}

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

@ -0,0 +1,34 @@
<!--
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="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">
</mat-form-field>
</div>

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

@ -0,0 +1,104 @@
///
/// 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, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
FilterPredicateType,
StringFilterPredicate,
StringOperation,
stringOperationTranslationMap
} from '@shared/models/query/query.models';
@Component({
selector: 'tb-string-filter-predicate',
templateUrl: './string-filter-predicate.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StringFilterPredicateComponent),
multi: true
}
]
})
export class StringFilterPredicateComponent implements ControlValueAccessor, OnInit {
@Input() disabled: boolean;
@Input() userMode: boolean;
stringFilterPredicateFormGroup: FormGroup;
stringOperations = Object.keys(StringOperation);
stringOperationEnum = StringOperation;
stringOperationTranslations = stringOperationTranslationMap;
private propagateChange = null;
constructor(private fb: FormBuilder) {
}
ngOnInit(): void {
this.stringFilterPredicateFormGroup = this.fb.group({
operation: [StringOperation.STARTS_WITH, [Validators.required]],
value: [''],
ignoreCase: [false]
});
if (this.userMode) {
this.stringFilterPredicateFormGroup.get('operation').disable({emitEvent: false});
this.stringFilterPredicateFormGroup.get('ignoreCase').disable({emitEvent: false});
}
this.stringFilterPredicateFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.stringFilterPredicateFormGroup.disable({emitEvent: false});
} else {
this.stringFilterPredicateFormGroup.enable({emitEvent: false});
}
}
writeValue(predicate: StringFilterPredicate): void {
this.stringFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false});
this.stringFilterPredicateFormGroup.get('value').patchValue(predicate.value ? predicate.value : '', {emitEvent: false});
this.stringFilterPredicateFormGroup.get('ignoreCase').patchValue(predicate.ignoreCase, {emitEvent: false});
}
private updateModel() {
let predicate: StringFilterPredicate = null;
if (this.stringFilterPredicateFormGroup.valid) {
predicate = this.stringFilterPredicateFormGroup.getRawValue();
if (!predicate.value) {
predicate.value = '';
}
predicate.type = FilterPredicateType.STRING;
}
this.propagateChange(predicate);
}
}

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

@ -66,6 +66,18 @@ import { SharedHomeComponentsModule } from '@home/components/shared-home-compone
import { SelectTargetLayoutDialogComponent } from '@home/components/dashboard/select-target-layout-dialog.component';
import { SelectTargetStateDialogComponent } from '@home/components/dashboard/select-target-state-dialog.component';
import { AliasesEntityAutocompleteComponent } from '@home/components/alias/aliases-entity-autocomplete.component';
import { BooleanFilterPredicateComponent } from '@home/components/filter/boolean-filter-predicate.component';
import { StringFilterPredicateComponent } from '@home/components/filter/string-filter-predicate.component';
import { NumericFilterPredicateComponent } from '@home/components/filter/numeric-filter-predicate.component';
import { ComplexFilterPredicateComponent } from '@home/components/filter/complex-filter-predicate.component';
import { FilterPredicateComponent } from '@home/components/filter/filter-predicate.component';
import { FilterPredicateListComponent } from '@home/components/filter/filter-predicate-list.component';
import { KeyFilterListComponent } from '@home/components/filter/key-filter-list.component';
import { ComplexFilterPredicateDialogComponent } from '@home/components/filter/complex-filter-predicate-dialog.component';
import { KeyFilterDialogComponent } from '@home/components/filter/key-filter-dialog.component';
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';
@NgModule({
declarations:
@ -114,7 +126,19 @@ import { AliasesEntityAutocompleteComponent } from '@home/components/alias/alias
SelectTargetLayoutDialogComponent,
SelectTargetStateDialogComponent,
AddWidgetToDashboardDialogComponent,
TableColumnsAssignmentComponent
TableColumnsAssignmentComponent,
BooleanFilterPredicateComponent,
StringFilterPredicateComponent,
NumericFilterPredicateComponent,
ComplexFilterPredicateComponent,
ComplexFilterPredicateDialogComponent,
FilterPredicateComponent,
FilterPredicateListComponent,
KeyFilterListComponent,
KeyFilterDialogComponent,
FilterDialogComponent,
FiltersDialogComponent,
FilterSelectComponent
],
imports: [
CommonModule,
@ -156,7 +180,19 @@ import { AliasesEntityAutocompleteComponent } from '@home/components/alias/alias
ImportDialogCsvComponent,
TableColumnsAssignmentComponent,
SelectTargetLayoutDialogComponent,
SelectTargetStateDialogComponent
SelectTargetStateDialogComponent,
BooleanFilterPredicateComponent,
StringFilterPredicateComponent,
NumericFilterPredicateComponent,
ComplexFilterPredicateComponent,
ComplexFilterPredicateDialogComponent,
FilterPredicateComponent,
FilterPredicateListComponent,
KeyFilterListComponent,
KeyFilterDialogComponent,
FilterDialogComponent,
FiltersDialogComponent,
FilterSelectComponent
],
providers: [
WidgetComponentService,

23
ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts

@ -55,6 +55,7 @@ import { RequestConfig } from '@core/http/http-utils';
import { RuleChain, RuleChainImport, RuleChainMetaData } from '@shared/models/rule-chain.models';
import { RuleChainService } from '@core/http/rule-chain.service';
import * as JSZip from 'jszip';
import { FiltersInfo } from '@shared/models/query/query.models';
// @dynamic
@Injectable()
@ -142,7 +143,8 @@ export class ImportExportService {
public importWidget(dashboard: Dashboard, targetState: string,
targetLayoutFunction: () => Observable<DashboardLayoutId>,
onAliasesUpdateFunction: () => void): Observable<ImportWidgetResult> {
onAliasesUpdateFunction: () => void,
onFiltersUpdateFunction: () => void): Observable<ImportWidgetResult> {
return this.openImportDialog('dashboard.import-widget', 'dashboard.widget-file').pipe(
mergeMap((widgetItem: WidgetItem) => {
if (!this.validateImportedWidget(widgetItem)) {
@ -154,6 +156,9 @@ export class ImportExportService {
let widget = widgetItem.widget;
widget = this.dashboardUtils.validateAndUpdateWidget(widget);
const aliasesInfo = this.prepareAliasesInfo(widgetItem.aliasesInfo);
const filtersInfo: FiltersInfo = widgetItem.filtersInfo || {
datasourceFilters: {}
};
const originalColumns = widgetItem.originalColumns;
const originalSize = widgetItem.originalSize;
@ -202,23 +207,23 @@ export class ImportExportService {
}
}
return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget,
aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize);
aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize);
}
));
} else {
return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget,
aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize);
aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize);
}
}
)
);
} else {
return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget,
aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize);
aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize);
}
} else {
return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget,
aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize);
aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize);
}
}
}),
@ -535,12 +540,16 @@ export class ImportExportService {
private addImportedWidget(dashboard: Dashboard, targetState: string,
targetLayoutFunction: () => Observable<DashboardLayoutId>,
widget: Widget, aliasesInfo: AliasesInfo, onAliasesUpdateFunction: () => void,
widget: Widget, aliasesInfo: AliasesInfo,
filtersInfo: FiltersInfo,
onAliasesUpdateFunction: () => void,
onFiltersUpdateFunction: () => void,
originalColumns: number, originalSize: WidgetSize): Observable<ImportWidgetResult> {
return targetLayoutFunction().pipe(
mergeMap((targetLayout) => {
return this.itembuffer.addWidgetToDashboard(dashboard, targetState, targetLayout,
widget, aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, -1, -1).pipe(
widget, aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction,
originalColumns, originalSize, -1, -1).pipe(
map(() => ({widget, layoutId: targetLayout} as ImportWidgetResult))
);
}

8
ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts

@ -155,10 +155,6 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
private dialog: MatDialog,
private fb: FormBuilder,
public truncate: TruncatePipe) {
this.keysListFormGroup = this.fb.group({
keys: [null, this.required ? [Validators.required] : []],
key: [null]
});
}
updateValidators() {
@ -174,6 +170,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
}
ngOnInit() {
this.keysListFormGroup = this.fb.group({
keys: [null, this.required ? [Validators.required] : []],
key: [null]
});
this.alarmKeys = [];
for (const name of Object.keys(alarmFields)) {
this.alarmKeys.push({

2
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts

@ -332,7 +332,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
const datasource = this.subscription.options.datasources ? this.subscription.options.datasources[0] : null;
if (datasource) {
if (datasource && datasource.dataKeys) {
datasource.dataKeys.forEach((entityDataKey) => {
const dataKey: EntityColumn = deepClone(entityDataKey) as EntityColumn;
dataKey.entityKey = {

28
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -154,12 +154,21 @@
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="datasourceType.entity">
<tb-entity-alias-select
[tbRequired]="datasourceControl.get('type').value === datasourceType.entity"
[aliasController]="aliasController"
formControlName="entityAliasId"
[callbacks]="widgetConfigCallbacks">
</tb-entity-alias-select>
<section fxLayout="column">
<tb-entity-alias-select
[showLabel]="true"
[tbRequired]="datasourceControl.get('type').value === datasourceType.entity"
[aliasController]="aliasController"
formControlName="entityAliasId"
[callbacks]="widgetConfigCallbacks">
</tb-entity-alias-select>
<tb-filter-select
[showLabel]="true"
[aliasController]="aliasController"
formControlName="filterId"
[callbacks]="widgetConfigCallbacks">
</tb-filter-select>
</section>
</ng-template>
</section>
<tb-data-keys class="tb-data-keys" fxFlex
@ -250,11 +259,18 @@
</ng-template>
<ng-template [ngSwitchCase]="datasourceType.entity">
<tb-entity-alias-select
[showLabel]="true"
[tbRequired]="alarmSourceSettings.get('type').value === datasourceType.entity"
[aliasController]="aliasController"
formControlName="entityAliasId"
[callbacks]="widgetConfigCallbacks">
</tb-entity-alias-select>
<tb-filter-select
[showLabel]="true"
[aliasController]="aliasController"
formControlName="filterId"
[callbacks]="widgetConfigCallbacks">
</tb-filter-select>
</ng-template>
</section>
<tb-data-keys class="tb-data-keys" fxFlex

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

@ -17,5 +17,6 @@
import { EntityAliasSelectCallbacks } from '../alias/entity-alias-select.component.models';
import { DataKeysCallbacks } from './data-keys.component.models';
import { WidgetActionCallbacks } from './action/manage-widget-actions.component.models';
import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models';
export type WidgetConfigCallbacks = EntityAliasSelectCallbacks & DataKeysCallbacks & WidgetActionCallbacks;
export type WidgetConfigCallbacks = EntityAliasSelectCallbacks & FilterSelectCallbacks & DataKeysCallbacks & WidgetActionCallbacks;

10
ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss

@ -93,7 +93,7 @@
.tb-datasource {
tb-entity-alias-select {
.mat-form-field {
margin-top: 13px;
margin-top: 20px;
}
@media #{$mat-gt-sm} {
.mat-form-field {
@ -102,6 +102,14 @@
}
}
}
tb-filter-select {
@media #{$mat-gt-sm} {
.mat-form-field {
width: 200px;
max-width: 200px;
}
}
}
}
.tb-data-keys {
@media #{$mat-gt-sm} {

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

@ -61,6 +61,8 @@ import { JsonFormComponentData } from '@shared/components/json-form/json-form-co
import { WidgetActionsData } from './action/manage-widget-actions.component.models';
import { DashboardState } from '@shared/models/dashboard.models';
import { entityFields } from '@shared/models/entity.models';
import { Filter, Filters } from '@shared/models/query/query.models';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
const emptySettingsSchema: JsonSchema = {
type: 'object',
@ -105,6 +107,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
@Input()
entityAliases: EntityAliases;
@Input()
filters: Filters;
@Input()
functionsOnly: boolean;
@ -121,6 +126,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
widgetConfigCallbacks: WidgetConfigCallbacks = {
createEntityAlias: this.createEntityAlias.bind(this),
createFilter: this.createFilter.bind(this),
generateDataKey: this.generateDataKey.bind(this),
fetchEntityKeys: this.fetchEntityKeys.bind(this),
fetchDashboardStates: this.fetchDashboardStates.bind(this)
@ -498,6 +504,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
name: [datasource ? datasource.name : null, []],
entityAliasId: [datasource ? datasource.entityAliasId : null,
datasource && datasource.type === DatasourceType.entity ? [Validators.required] : []],
filterId: [datasource ? datasource.filterId : null, []],
dataKeys: [datasource ? datasource.dataKeys : null, dataKeysRequired ? [Validators.required] : []]
}
);
@ -713,6 +720,28 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
);
}
private createFilter(filter: string): Observable<Filter> {
const singleFilter: Filter = {id: null, filter, keyFilters: []};
return this.dialog.open<FilterDialogComponent, FilterDialogData,
Filter>(FilterDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd: true,
filters: this.filters,
filter: singleFilter,
userMode: false
}
}).afterClosed().pipe(
tap((result) => {
if (result) {
this.filters[result.id] = result;
this.aliasController.updateFilters(this.filters);
}
})
);
}
private fetchEntityKeys(entityAliasId: string, query: string, dataKeyTypes: Array<DataKeyType>): Observable<Array<DataKey>> {
return this.aliasController.resolveSingleEntityInfo(entityAliasId).pipe(
mergeMap((entity) => {
@ -805,7 +834,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
};
}
} else if (this.widgetType === widgetType.alarm && this.modelValue.isDataEnabled) {
if (!config.alarmSource) {
if (!this.alarmSourceSettings.valid || !config.alarmSource) {
return {
alarmSource: {
valid: false

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

@ -640,6 +640,20 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
));
this.rxSubscriptions.push(this.widgetContext.aliasController.filtersChanged.subscribe(
(filterIds) => {
let subscriptionChanged = false;
for (const id of Object.keys(this.widgetContext.subscriptions)) {
const subscription = this.widgetContext.subscriptions[id];
subscriptionChanged = subscriptionChanged || subscription.onFiltersChanged(filterIds);
}
if (subscriptionChanged && !this.typeParameters.useCustomDatasources) {
this.displayNoData = false;
this.reInit();
}
}
));
this.rxSubscriptions.push(this.widgetContext.dashboard.dashboardTimewindowChanged.subscribe(
(dashboardTimewindow) => {
for (const id of Object.keys(this.widgetContext.subscriptions)) {

1
ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.html

@ -34,6 +34,7 @@
[aliasController]="aliasController"
[functionsOnly]="false"
[entityAliases]="dashboard.configuration.entityAliases"
[filters]="dashboard.configuration.filters"
[dashboardStates]="dashboard.configuration.states"
formControlName="widgetConfig">
</tb-widget-config>

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

@ -95,6 +95,12 @@
tooltipPosition="below"
[aliasController]="dashboardCtx.aliasController">
</tb-aliases-entity-select>
<button [fxShow]="isEdit" mat-icon-button
matTooltip="{{ 'filter.filters' | translate }}"
matTooltipPosition="below"
(click)="openFilters($event)">
<mat-icon>filter_list</mat-icon>
</button>
<button [fxShow]="isEdit" mat-icon-button
matTooltip="{{ 'entity.aliases' | translate }}"
matTooltipPosition="below"

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

@ -87,6 +87,8 @@ import {
} from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component';
import { ImportExportService } from '@home/components/import-export/import-export.service';
import { AuthState } from '@app/core/auth/auth.models';
import { FiltersDialogComponent, FiltersDialogData } from '@home/components/filter/filters-dialog.component';
import { Filters } from '@shared/models/query/query.models';
// @dynamic
@Component({
@ -277,7 +279,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.dashboardCtx.aliasController = new AliasController(this.utils,
this.entityService,
() => this.dashboardCtx.stateController,
this.dashboardConfiguration.entityAliases);
this.dashboardConfiguration.entityAliases,
this.dashboardConfiguration.filters);
if (this.widgetEditMode) {
const message: WindowMessage = {
@ -491,6 +494,27 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
});
}
public openFilters($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<FiltersDialogComponent, FiltersDialogData,
Filters>(FiltersDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
filters: deepClone(this.dashboard.configuration.filters),
widgets: this.dashboardUtils.getWidgetsArray(this.dashboard),
isSingleFilter: false
}
}).afterClosed().subscribe((filters) => {
if (filters) {
this.dashboard.configuration.filters = filters;
this.filtersUpdated();
}
});
}
public openDashboardSettings($event: Event) {
if ($event) {
$event.stopPropagation();
@ -577,7 +601,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
$event.stopPropagation();
}
this.importExport.importWidget(this.dashboard, this.dashboardCtx.state,
this.selectTargetLayout.bind(this), this.entityAliasesUpdated.bind(this)).subscribe(
this.selectTargetLayout.bind(this), this.entityAliasesUpdated.bind(this), this.filtersUpdated.bind(this)).subscribe(
(importData) => {
if (importData) {
const widget = importData.widget;
@ -667,6 +691,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.dashboardConfiguration = this.dashboard.configuration;
this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
this.entityAliasesUpdated();
this.filtersUpdated();
this.updateLayouts();
} else {
this.dashboard.configuration.timewindow = this.dashboardCtx.dashboardTimewindow;
@ -689,6 +714,10 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.dashboardCtx.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases);
}
private filtersUpdated() {
this.dashboardCtx.aliasController.updateFilters(this.dashboard.configuration.filters);
}
private notifyDashboardUpdated() {
if (this.widgetEditMode) {
const widget = this.layouts.main.layoutCtx.widgets.widgetByIndex(0);
@ -894,7 +923,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) {
this.itembuffer.pasteWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id,
pos, this.entityAliasesUpdated.bind(this)).subscribe(
pos, this.entityAliasesUpdated.bind(this), this.filtersUpdated.bind(this)).subscribe(
(widget) => {
layoutCtx.widgets.addWidgetId(widget.id);
});

1
ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.html

@ -21,6 +21,7 @@
[aliasController]="aliasController"
[functionsOnly]="widgetEditMode"
[entityAliases]="dashboard.configuration.entityAliases"
[filters]="dashboard.configuration.filters"
[dashboardStates]="dashboard.configuration.states"
formControlName="widgetConfig">
</tb-widget-config>

1
ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts

@ -90,6 +90,7 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
}
} as IStateController;
},
{},
{});
@ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent;

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

@ -21,6 +21,7 @@ import { ShortCustomerInfo } from '@shared/models/customer.model';
import { Widget } from './widget.models';
import { Timewindow } from '@shared/models/time/time.models';
import { EntityAliases } from './alias.models';
import { Filters } from '@shared/models/query/query.models';
export interface DashboardInfo extends BaseData<DashboardId> {
tenantId?: TenantId;
@ -96,6 +97,7 @@ export interface DashboardConfiguration {
widgets?: {[id: string]: Widget } | Widget[];
states?: {[id: string]: DashboardState };
entityAliases?: EntityAliases;
filters?: Filters;
[key: string]: any;
}

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

@ -33,6 +33,14 @@ export enum EntityKeyType {
ENTITY_FIELD = 'ENTITY_FIELD'
}
export const entityKeyTypeTranslationMap = new Map<EntityKeyType, string>(
[
[EntityKeyType.ATTRIBUTE, 'filter.key-type.attribute'],
[EntityKeyType.TIME_SERIES, 'filter.key-type.timeseries'],
[EntityKeyType.ENTITY_FIELD, 'filter.key-type.entity-field']
]
);
export function entityKeyTypeToDataKeyType(entityKeyType: EntityKeyType): DataKeyType {
switch (entityKeyType) {
case EntityKeyType.ATTRIBUTE:
@ -52,6 +60,80 @@ export interface EntityKey {
key: string;
}
export enum EntityKeyValueType {
STRING = 'STRING',
NUMERIC = 'NUMERIC',
BOOLEAN = 'BOOLEAN'
}
export interface EntityKeyValueTypeData {
name: string;
icon: string;
}
export const entityKeyValueTypesMap = new Map<EntityKeyValueType, EntityKeyValueTypeData>(
[
[
EntityKeyValueType.STRING,
{
name: 'filter.value-type.string',
icon: 'mdi:format-text'
}
],
[
EntityKeyValueType.NUMERIC,
{
name: 'filter.value-type.numeric',
icon: 'mdi:numeric'
}
],
[
EntityKeyValueType.BOOLEAN,
{
name: 'filter.value-type.boolean',
icon: 'mdi:checkbox-marked-outline'
}
]
]
);
export function entityKeyValueTypeToFilterPredicateType(valueType: EntityKeyValueType): FilterPredicateType {
switch (valueType) {
case EntityKeyValueType.STRING:
return FilterPredicateType.STRING;
case EntityKeyValueType.NUMERIC:
return FilterPredicateType.NUMERIC;
case EntityKeyValueType.BOOLEAN:
return FilterPredicateType.BOOLEAN;
}
}
export function createDefaultFilterPredicate(valueType: EntityKeyValueType, complex: boolean): KeyFilterPredicate {
const predicate = {
type: complex ? FilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType)
} as KeyFilterPredicate;
switch (predicate.type) {
case FilterPredicateType.STRING:
predicate.operation = StringOperation.STARTS_WITH;
predicate.value = '';
predicate.ignoreCase = false;
break;
case FilterPredicateType.NUMERIC:
predicate.operation = NumericOperation.EQUAL;
predicate.value = 0;
break;
case FilterPredicateType.BOOLEAN:
predicate.operation = BooleanOperation.EQUAL;
predicate.value = false;
break;
case FilterPredicateType.COMPLEX:
predicate.operation = ComplexOperation.AND;
predicate.predicates = [];
break;
}
return predicate;
}
export enum FilterPredicateType {
STRING = 'STRING',
NUMERIC = 'NUMERIC',
@ -68,6 +150,17 @@ export enum StringOperation {
NOT_CONTAIN = 'NOT_CONTAIN'
}
export const stringOperationTranslationMap = new Map<StringOperation, string>(
[
[StringOperation.EQUAL, 'filter.operation.equal'],
[StringOperation.NOT_EQUAL, 'filter.operation.not-equal'],
[StringOperation.STARTS_WITH, 'filter.operation.starts-with'],
[StringOperation.ENDS_WITH, 'filter.operation.ends-with'],
[StringOperation.CONTAINS, 'filter.operation.contains'],
[StringOperation.NOT_CONTAIN, 'filter.operation.not-contain']
]
);
export enum NumericOperation {
EQUAL = 'EQUAL',
NOT_EQUAL = 'NOT_EQUAL',
@ -77,16 +170,41 @@ export enum NumericOperation {
LESS_OR_EQUAL = 'LESS_OR_EQUAL'
}
export const numericOperationTranslationMap = new Map<NumericOperation, string>(
[
[NumericOperation.EQUAL, 'filter.operation.equal'],
[NumericOperation.NOT_EQUAL, 'filter.operation.not-equal'],
[NumericOperation.GREATER, 'filter.operation.greater'],
[NumericOperation.LESS, 'filter.operation.less'],
[NumericOperation.GREATER_OR_EQUAL, 'filter.operation.greater-or-equal'],
[NumericOperation.LESS_OR_EQUAL, 'filter.operation.less-or-equal']
]
);
export enum BooleanOperation {
EQUAL = 'EQUAL',
NOT_EQUAL = 'NOT_EQUAL'
}
export const booleanOperationTranslationMap = new Map<BooleanOperation, string>(
[
[BooleanOperation.EQUAL, 'filter.operation.equal'],
[BooleanOperation.NOT_EQUAL, 'filter.operation.not-equal']
]
);
export enum ComplexOperation {
AND = 'AND',
OR = 'OR'
}
export const complexOperationTranslationMap = new Map<ComplexOperation, string>(
[
[ComplexOperation.AND, 'filter.operation.and'],
[ComplexOperation.OR, 'filter.operation.or']
]
);
export interface StringFilterPredicate {
type: FilterPredicateType.STRING,
operation: StringOperation;
@ -122,6 +240,45 @@ export interface KeyFilter {
predicate: KeyFilterPredicate;
}
export interface KeyFilterInfo {
key: EntityKey;
valueType: EntityKeyValueType;
predicates: Array<KeyFilterPredicate>;
}
export interface FilterInfo {
filter: string;
keyFilters: Array<KeyFilterInfo>;
}
export interface FiltersInfo {
datasourceFilters: {[datasourceIndex: number]: FilterInfo};
}
export function filterInfoToKeyFilters(filter: FilterInfo): Array<KeyFilter> {
const keyFilterInfos = filter.keyFilters;
const keyFilters: Array<KeyFilter> = [];
for (const keyFilterInfo of keyFilterInfos) {
const key = keyFilterInfo.key;
for (const predicate of keyFilterInfo.predicates) {
const keyFilter: KeyFilter = {
key,
predicate
};
keyFilters.push(keyFilter);
}
}
return keyFilters;
}
export interface Filter extends FilterInfo {
id: string;
}
export interface Filters {
[id: string]: Filter
}
export interface EntityFilter extends EntityFilters {
type?: AliasFilterType;
}

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

@ -262,6 +262,7 @@ export interface Datasource {
entityId?: string;
entityName?: string;
entityAliasId?: string;
filterId?: string;
unresolvedStateEntity?: boolean;
dataReceived?: boolean;
entity?: BaseData<EntityId>;

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

@ -1154,6 +1154,67 @@
"file": "Extensions file",
"invalid-file-error": "Invalid extension file"
},
"filter": {
"add": "Add filter",
"edit": "Edit filter",
"name": "Filter name",
"name-required": "Filter name is required.",
"duplicate-filter": "Filter with same name is already exists.",
"filters": "Filters",
"unable-delete-filter-title": "Unable to delete filter",
"unable-delete-filter-text": "Filter '{{filter}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
"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",
"no-filters-found": "No filters found.",
"no-filter-matching": "'{{filter}}' not found.",
"create-new-filter": "Create a new one!",
"filter-required": "Filter is required.",
"operation": {
"operation": "Operation",
"equal": "Equal",
"not-equal": "Not equal",
"starts-with": "Starts with",
"ends-with": "Ends with",
"contains": "Contains",
"not-contain": "Not contain",
"greater": "Greater than",
"less": "Less than",
"greater-or-equal": "Greater or equal",
"less-or-equal": "Less or equal",
"and": "And",
"or": "Or"
},
"ignore-case": "Ignore case",
"value": "Value",
"remove-filter": "Remove filter",
"no-filters": "No filters configured",
"add-filter": "Add filter",
"add-complex-filter": "Add complex filter",
"add-complex": "Add complex",
"complex-filter": "Complex filter",
"edit-complex-filter": "Edit complex filter",
"key-filter": "Key filter",
"key-name": "Key name",
"key-name-required": "Key name is required.",
"key-type": {
"key-type": "Key type",
"attribute": "Attribute",
"timeseries": "Timeseries",
"entity-field": "Entity field"
},
"value-type": {
"value-type": "Value type",
"string": "String",
"numeric": "Numeric",
"boolean": "Boolean"
},
"value-type-required": "Key value type is required.",
"no-key-filters": "No key filters configured",
"add-key-filter": "Add key filter",
"remove-key-filter": "Remove key filter",
"edit-key-filter": "Edit key filter"
},
"fullscreen": {
"expand": "Expand to fullscreen",
"exit": "Exit fullscreen",

Loading…
Cancel
Save