Commit d987c864564cc5fc9dee6b47e85389ccf0139d2e

Authored by Igor Kulikov
1 parent 3cee4174

UI: Widget Config. Entity aliases dialog.

Showing 53 changed files with 3607 additions and 106 deletions
@@ -1622,7 +1622,6 @@ @@ -1622,7 +1622,6 @@
1622 "version": "1.0.10", 1622 "version": "1.0.10",
1623 "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 1623 "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
1624 "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 1624 "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
1625 - "dev": true,  
1626 "requires": { 1625 "requires": {
1627 "sprintf-js": "~1.0.2" 1626 "sprintf-js": "~1.0.2"
1628 } 1627 }
@@ -5308,9 +5307,9 @@ @@ -5308,9 +5307,9 @@
5308 "dev": true 5307 "dev": true
5309 }, 5308 },
5310 "handlebars": { 5309 "handlebars": {
5311 - "version": "4.1.2",  
5312 - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",  
5313 - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", 5310 + "version": "4.4.2",
  5311 + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.2.tgz",
  5312 + "integrity": "sha512-cIv17+GhL8pHHnRJzGu2wwcthL5sb8uDKBHvZ2Dtu5s1YNt0ljbzKbamnc+gr69y7bzwQiBdr5+hOpRd5pnOdg==",
5314 "dev": true, 5313 "dev": true,
5315 "requires": { 5314 "requires": {
5316 "neo-async": "^2.6.0", 5315 "neo-async": "^2.6.0",
@@ -6532,6 +6531,14 @@ @@ -6532,6 +6531,14 @@
6532 "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", 6531 "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
6533 "dev": true 6532 "dev": true
6534 }, 6533 },
  6534 + "json-schema-defaults": {
  6535 + "version": "0.4.0",
  6536 + "resolved": "https://registry.npmjs.org/json-schema-defaults/-/json-schema-defaults-0.4.0.tgz",
  6537 + "integrity": "sha512-UsUrkDVNvHTneyeQOYHH9ZHb3+6OjwYfJ831SdO0yjtXtYZ7Jh8BKWsuJYUQW7qckP5JhHawsg4GI6A5fMaR/Q==",
  6538 + "requires": {
  6539 + "argparse": "^1.0.9"
  6540 + }
  6541 + },
6535 "json-schema-traverse": { 6542 "json-schema-traverse": {
6536 "version": "0.4.1", 6543 "version": "0.4.1",
6537 "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 6544 "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -9989,8 +9996,7 @@ @@ -9989,8 +9996,7 @@
9989 "sprintf-js": { 9996 "sprintf-js": {
9990 "version": "1.0.3", 9997 "version": "1.0.3",
9991 "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 9998 "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
9992 - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",  
9993 - "dev": true 9999 + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
9994 }, 10000 },
9995 "sshpk": { 10001 "sshpk": {
9996 "version": "1.16.1", 10002 "version": "1.16.1",
@@ -46,6 +46,7 @@ @@ -46,6 +46,7 @@
46 "jquery": "^3.4.1", 46 "jquery": "^3.4.1",
47 "jquery.terminal": "^2.8.0", 47 "jquery.terminal": "^2.8.0",
48 "js-beautify": "^1.10.2", 48 "js-beautify": "^1.10.2",
  49 + "json-schema-defaults": "^0.4.0",
49 "material-design-icons": "^3.0.1", 50 "material-design-icons": "^3.0.1",
50 "messageformat": "^2.3.0", 51 "messageformat": "^2.3.0",
51 "moment": "^2.24.0", 52 "moment": "^2.24.0",
@@ -21,7 +21,8 @@ import {HttpClient} from '@angular/common/http'; @@ -21,7 +21,8 @@ import {HttpClient} from '@angular/common/http';
21 import {PageLink} from '@shared/models/page/page-link'; 21 import {PageLink} from '@shared/models/page/page-link';
22 import {PageData} from '@shared/models/page/page-data'; 22 import {PageData} from '@shared/models/page/page-data';
23 import {EntitySubtype} from '@app/shared/models/entity-type.models'; 23 import {EntitySubtype} from '@app/shared/models/entity-type.models';
24 -import {Asset, AssetInfo} from '@app/shared/models/asset.models'; 24 +import {Asset, AssetInfo, AssetSearchQuery} from '@app/shared/models/asset.models';
  25 +import { Device, DeviceSearchQuery } from '@shared/models/device.models';
25 26
26 @Injectable({ 27 @Injectable({
27 providedIn: 'root' 28 providedIn: 'root'
@@ -81,4 +82,9 @@ export class AssetService { @@ -81,4 +82,9 @@ export class AssetService {
81 return this.http.delete(`/api/customer/asset/${assetId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); 82 return this.http.delete(`/api/customer/asset/${assetId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
82 } 83 }
83 84
  85 + public findByQuery(query: AssetSearchQuery,
  86 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<Asset>> {
  87 + return this.http.post<Array<Asset>>('/api/assets', query, defaultHttpOptions(ignoreLoading, ignoreErrors));
  88 + }
  89 +
84 } 90 }
@@ -23,7 +23,7 @@ import { PageData } from '@shared/models/page/page-data'; @@ -23,7 +23,7 @@ import { PageData } from '@shared/models/page/page-data';
23 import { Tenant } from '@shared/models/tenant.model'; 23 import { Tenant } from '@shared/models/tenant.model';
24 import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models'; 24 import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models';
25 import {map} from 'rxjs/operators'; 25 import {map} from 'rxjs/operators';
26 -import {DeviceInfo, Device, DeviceCredentials} from '@app/shared/models/device.models'; 26 +import { DeviceInfo, Device, DeviceCredentials, DeviceSearchQuery } from '@app/shared/models/device.models';
27 import {EntitySubtype} from '@app/shared/models/entity-type.models'; 27 import {EntitySubtype} from '@app/shared/models/entity-type.models';
28 import {AuthService} from '../auth/auth.service'; 28 import {AuthService} from '../auth/auth.service';
29 29
@@ -115,4 +115,9 @@ export class DeviceService { @@ -115,4 +115,9 @@ export class DeviceService {
115 return this.http.delete(`/api/customer/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); 115 return this.http.delete(`/api/customer/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
116 } 116 }
117 117
  118 + public findByQuery(query: DeviceSearchQuery,
  119 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<Device>> {
  120 + return this.http.post<Array<Device>>('/api/devices', query, defaultHttpOptions(ignoreLoading, ignoreErrors));
  121 + }
  122 +
118 } 123 }
@@ -21,7 +21,8 @@ import {HttpClient} from '@angular/common/http'; @@ -21,7 +21,8 @@ import {HttpClient} from '@angular/common/http';
21 import {PageLink} from '@shared/models/page/page-link'; 21 import {PageLink} from '@shared/models/page/page-link';
22 import {PageData} from '@shared/models/page/page-data'; 22 import {PageData} from '@shared/models/page/page-data';
23 import {EntitySubtype} from '@app/shared/models/entity-type.models'; 23 import {EntitySubtype} from '@app/shared/models/entity-type.models';
24 -import {EntityView, EntityViewInfo} from '@app/shared/models/entity-view.models'; 24 +import { EntityView, EntityViewInfo, EntityViewSearchQuery } from '@app/shared/models/entity-view.models';
  25 +import { Asset, AssetSearchQuery } from '@shared/models/asset.models';
25 26
26 @Injectable({ 27 @Injectable({
27 providedIn: 'root' 28 providedIn: 'root'
@@ -80,4 +81,9 @@ export class EntityViewService { @@ -80,4 +81,9 @@ export class EntityViewService {
80 return this.http.delete(`/api/customer/entityView/${entityViewId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); 81 return this.http.delete(`/api/customer/entityView/${entityViewId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
81 } 82 }
82 83
  84 + public findByQuery(query: EntityViewSearchQuery,
  85 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityView>> {
  86 + return this.http.post<Array<EntityView>>('/api/entityViews', query, defaultHttpOptions(ignoreLoading, ignoreErrors));
  87 + }
  88 +
83 } 89 }
@@ -33,16 +33,29 @@ import { Store } from '@ngrx/store'; @@ -33,16 +33,29 @@ import { Store } from '@ngrx/store';
33 import { AppState } from '@core/core.state'; 33 import { AppState } from '@core/core.state';
34 import { Authority } from '@shared/models/authority.enum'; 34 import { Authority } from '@shared/models/authority.enum';
35 import { Tenant } from '@shared/models/tenant.model'; 35 import { Tenant } from '@shared/models/tenant.model';
36 -import { catchError, concatMap, expand, map, toArray } from 'rxjs/operators'; 36 +import { catchError, concatMap, expand, map, mergeMap, toArray } from 'rxjs/operators';
37 import { Customer } from '@app/shared/models/customer.model'; 37 import { Customer } from '@app/shared/models/customer.model';
38 import { AssetService } from '@core/http/asset.service'; 38 import { AssetService } from '@core/http/asset.service';
39 import { EntityViewService } from '@core/http/entity-view.service'; 39 import { EntityViewService } from '@core/http/entity-view.service';
40 import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; 40 import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
41 import { defaultHttpOptions } from '@core/http/http-utils'; 41 import { defaultHttpOptions } from '@core/http/http-utils';
42 import { RuleChainService } from '@core/http/rule-chain.service'; 42 import { RuleChainService } from '@core/http/rule-chain.service';
43 -import { SubscriptionInfo } from '@core/api/widget-api.models'; 43 +import { StateParams, SubscriptionInfo } from '@core/api/widget-api.models';
44 import { Datasource, DatasourceType, KeyInfo } from '@app/shared/models/widget.models'; 44 import { Datasource, DatasourceType, KeyInfo } from '@app/shared/models/widget.models';
45 import { UtilsService } from '@core/services/utils.service'; 45 import { UtilsService } from '@core/services/utils.service';
  46 +import { AliasFilterType, EntityAliasFilter, EntityAliasFilterResult } from '@shared/models/alias.models';
  47 +import { EntityInfo } from '@shared/models/entity.models';
  48 +import {
  49 + EntityRelationInfo,
  50 + EntityRelationsQuery,
  51 + EntitySearchDirection,
  52 + EntitySearchQuery
  53 +} from '@shared/models/relation.models';
  54 +import { EntityRelationService } from '@core/http/entity-relation.service';
  55 +import { isDefined } from '../utils';
  56 +import { AssetSearchQuery } from '@shared/models/asset.models';
  57 +import { DeviceSearchQuery } from '@shared/models/device.models';
  58 +import { EntityViewSearchQuery } from '@shared/models/entity-view.models';
46 59
47 @Injectable({ 60 @Injectable({
48 providedIn: 'root' 61 providedIn: 'root'
@@ -60,6 +73,7 @@ export class EntityService { @@ -60,6 +73,7 @@ export class EntityService {
60 private userService: UserService, 73 private userService: UserService,
61 private ruleChainService: RuleChainService, 74 private ruleChainService: RuleChainService,
62 private dashboardService: DashboardService, 75 private dashboardService: DashboardService,
  76 + private entityRelationService: EntityRelationService,
63 private utils: UtilsService 77 private utils: UtilsService
64 ) { } 78 ) { }
65 79
@@ -348,8 +362,62 @@ export class EntityService { @@ -348,8 +362,62 @@ export class EntityService {
348 } 362 }
349 } 363 }
350 364
  365 + public getAliasFilterTypesByEntityTypes(entityTypes: Array<EntityType | AliasEntityType>): Array<AliasFilterType> {
  366 + const allAliasFilterTypes: Array<AliasFilterType> = Object.keys(AliasFilterType).map((key) => AliasFilterType[key]);
  367 + if (!entityTypes || !entityTypes.length) {
  368 + return allAliasFilterTypes;
  369 + }
  370 + const result = [];
  371 + for (const aliasFilterType of allAliasFilterTypes) {
  372 + if (this.filterAliasFilterTypeByEntityTypes(aliasFilterType, entityTypes)) {
  373 + result.push(aliasFilterType);
  374 + }
  375 + }
  376 + return result;
  377 + }
  378 +
  379 + private filterAliasFilterTypeByEntityTypes(aliasFilterType: AliasFilterType,
  380 + entityTypes: Array<EntityType | AliasEntityType>): boolean {
  381 + if (!entityTypes || !entityTypes.length) {
  382 + return true;
  383 + }
  384 + let valid = false;
  385 + entityTypes.forEach((entityType) => {
  386 + valid = valid || this.filterAliasFilterTypeByEntityType(aliasFilterType, entityType);
  387 + });
  388 + return valid;
  389 + }
  390 +
  391 + private filterAliasFilterTypeByEntityType(aliasFilterType: AliasFilterType, entityType: EntityType | AliasEntityType): boolean {
  392 + switch (aliasFilterType) {
  393 + case AliasFilterType.singleEntity:
  394 + return true;
  395 + case AliasFilterType.entityList:
  396 + return true;
  397 + case AliasFilterType.entityName:
  398 + return true;
  399 + case AliasFilterType.stateEntity:
  400 + return true;
  401 + case AliasFilterType.assetType:
  402 + return entityType === EntityType.ASSET;
  403 + case AliasFilterType.deviceType:
  404 + return entityType === EntityType.DEVICE;
  405 + case AliasFilterType.entityViewType:
  406 + return entityType === EntityType.ENTITY_VIEW;
  407 + case AliasFilterType.relationsQuery:
  408 + return true;
  409 + case AliasFilterType.assetSearchQuery:
  410 + return entityType === EntityType.ASSET;
  411 + case AliasFilterType.deviceSearchQuery:
  412 + return entityType === EntityType.DEVICE;
  413 + case AliasFilterType.entityViewSearchQuery:
  414 + return entityType === EntityType.ENTITY_VIEW;
  415 + }
  416 + return false;
  417 + }
  418 +
351 public prepareAllowedEntityTypesList(allowedEntityTypes: Array<EntityType | AliasEntityType>, 419 public prepareAllowedEntityTypesList(allowedEntityTypes: Array<EntityType | AliasEntityType>,
352 - useAliasEntityTypes: boolean): Array<EntityType | AliasEntityType> { 420 + useAliasEntityTypes?: boolean): Array<EntityType | AliasEntityType> {
353 const authUser = getCurrentAuthUser(this.store); 421 const authUser = getCurrentAuthUser(this.store);
354 const entityTypes: Array<EntityType | AliasEntityType> = []; 422 const entityTypes: Array<EntityType | AliasEntityType> = [];
355 switch (authUser.authority) { 423 switch (authUser.authority) {
@@ -444,6 +512,285 @@ export class EntityService { @@ -444,6 +512,285 @@ export class EntityService {
444 } 512 }
445 } 513 }
446 514
  515 + public resolveAliasFilter(filter: EntityAliasFilter, stateParams: StateParams,
  516 + maxItems: number, failOnEmpty: boolean): Observable<EntityAliasFilterResult> {
  517 + const result: EntityAliasFilterResult = {
  518 + entities: [],
  519 + stateEntity: false
  520 + };
  521 + if (filter.stateEntityParamName && filter.stateEntityParamName.length) {
  522 + result.entityParamName = filter.stateEntityParamName;
  523 + }
  524 + const stateEntityId = this.getStateEntityId(filter, stateParams);
  525 + switch (filter.type) {
  526 + case AliasFilterType.singleEntity:
  527 + const aliasEntityId = this.resolveAliasEntityId(filter.singleEntity.entityType, filter.singleEntity.id);
  528 + return this.getEntity(aliasEntityId.entityType as EntityType, aliasEntityId.id, true, true).pipe(
  529 + map((entity) => {
  530 + result.entities = this.entitiesToEntitiesInfo([entity]);
  531 + return result;
  532 + }
  533 + ));
  534 + break;
  535 + case AliasFilterType.entityList:
  536 + return this.getEntities(filter.entityType, filter.entityList, true, true).pipe(
  537 + map((entities) => {
  538 + if (entities && entities.length || !failOnEmpty) {
  539 + result.entities = this.entitiesToEntitiesInfo(entities);
  540 + return result;
  541 + } else {
  542 + throw new Error();
  543 + }
  544 + }
  545 + ));
  546 + break;
  547 + case AliasFilterType.entityName:
  548 + return this.getEntitiesByNameFilter(filter.entityType, filter.entityNameFilter, maxItems,
  549 + '', true, true).pipe(
  550 + map((entities) => {
  551 + if (entities && entities.length || !failOnEmpty) {
  552 + result.entities = this.entitiesToEntitiesInfo(entities);
  553 + return result;
  554 + } else {
  555 + throw new Error();
  556 + }
  557 + }
  558 + )
  559 + );
  560 + break;
  561 + case AliasFilterType.stateEntity:
  562 + result.stateEntity = true;
  563 + if (stateEntityId) {
  564 + return this.getEntity(stateEntityId.entityType as EntityType, stateEntityId.id, true, true).pipe(
  565 + map((entity) => {
  566 + result.entities = this.entitiesToEntitiesInfo([entity]);
  567 + return result;
  568 + }
  569 + ));
  570 + } else {
  571 + return of(result);
  572 + }
  573 + break;
  574 + case AliasFilterType.assetType:
  575 + return this.getEntitiesByNameFilter(EntityType.ASSET, filter.assetNameFilter, maxItems,
  576 + filter.assetType, true, true).pipe(
  577 + map((entities) => {
  578 + if (entities && entities.length || !failOnEmpty) {
  579 + result.entities = this.entitiesToEntitiesInfo(entities);
  580 + return result;
  581 + } else {
  582 + throw new Error();
  583 + }
  584 + }
  585 + )
  586 + );
  587 + break;
  588 + case AliasFilterType.deviceType:
  589 + return this.getEntitiesByNameFilter(EntityType.DEVICE, filter.deviceNameFilter, maxItems,
  590 + filter.deviceType, true, true).pipe(
  591 + map((entities) => {
  592 + if (entities && entities.length || !failOnEmpty) {
  593 + result.entities = this.entitiesToEntitiesInfo(entities);
  594 + return result;
  595 + } else {
  596 + throw new Error();
  597 + }
  598 + }
  599 + )
  600 + );
  601 + break;
  602 + case AliasFilterType.entityViewType:
  603 + return this.getEntitiesByNameFilter(EntityType.ENTITY_VIEW, filter.entityViewNameFilter, maxItems,
  604 + filter.entityViewType, true, true).pipe(
  605 + map((entities) => {
  606 + if (entities && entities.length || !failOnEmpty) {
  607 + result.entities = this.entitiesToEntitiesInfo(entities);
  608 + return result;
  609 + } else {
  610 + throw new Error();
  611 + }
  612 + }
  613 + )
  614 + );
  615 + break;
  616 + case AliasFilterType.relationsQuery:
  617 + result.stateEntity = filter.rootStateEntity;
  618 + let rootEntityType;
  619 + let rootEntityId;
  620 + if (result.stateEntity && stateEntityId) {
  621 + rootEntityType = stateEntityId.entityType;
  622 + rootEntityId = stateEntityId.id;
  623 + } else if (!result.stateEntity) {
  624 + rootEntityType = filter.rootEntity.entityType;
  625 + rootEntityId = filter.rootEntity.id;
  626 + }
  627 + if (rootEntityType && rootEntityId) {
  628 + const relationQueryRootEntityId = this.resolveAliasEntityId(rootEntityType, rootEntityId);
  629 + const searchQuery: EntityRelationsQuery = {
  630 + parameters: {
  631 + rootId: relationQueryRootEntityId.id,
  632 + rootType: relationQueryRootEntityId.entityType as EntityType,
  633 + direction: filter.direction
  634 + },
  635 + filters: filter.filters
  636 + };
  637 + searchQuery.parameters.maxLevel = filter.maxLevel && filter.maxLevel > 0 ? filter.maxLevel : -1;
  638 + return this.entityRelationService.findInfoByQuery(searchQuery, true, true).pipe(
  639 + mergeMap((allRelations) => {
  640 + if (allRelations && allRelations.length || !failOnEmpty) {
  641 + if (isDefined(maxItems) && maxItems > 0 && allRelations) {
  642 + const limit = Math.min(allRelations.length, maxItems);
  643 + allRelations.length = limit;
  644 + }
  645 + return this.entityRelationInfosToEntitiesInfo(allRelations, filter.direction).pipe(
  646 + map((entities) => {
  647 + result.entities = entities;
  648 + return result;
  649 + })
  650 + );
  651 + } else {
  652 + return throwError(null);
  653 + }
  654 + })
  655 + );
  656 + } else {
  657 + return of(result);
  658 + }
  659 + break;
  660 + case AliasFilterType.assetSearchQuery:
  661 + case AliasFilterType.deviceSearchQuery:
  662 + case AliasFilterType.entityViewSearchQuery:
  663 + result.stateEntity = filter.rootStateEntity;
  664 + if (result.stateEntity && stateEntityId) {
  665 + rootEntityType = stateEntityId.entityType;
  666 + rootEntityId = stateEntityId.id;
  667 + } else if (!result.stateEntity) {
  668 + rootEntityType = filter.rootEntity.entityType;
  669 + rootEntityId = filter.rootEntity.id;
  670 + }
  671 + if (rootEntityType && rootEntityId) {
  672 + const searchQueryRootEntityId = this.resolveAliasEntityId(rootEntityType, rootEntityId);
  673 + const searchQuery: EntitySearchQuery = {
  674 + parameters: {
  675 + rootId: searchQueryRootEntityId.id,
  676 + rootType: searchQueryRootEntityId.entityType as EntityType,
  677 + direction: filter.direction
  678 + },
  679 + relationType: filter.relationType
  680 + };
  681 + let findByQueryObservable: Observable<Array<BaseData<EntityId>>>;
  682 + if (filter.type === AliasFilterType.assetSearchQuery) {
  683 + const assetSearchQuery = searchQuery as AssetSearchQuery;
  684 + assetSearchQuery.assetTypes = filter.assetTypes;
  685 + findByQueryObservable = this.assetService.findByQuery(assetSearchQuery, true, true);
  686 + } else if (filter.type === AliasFilterType.deviceSearchQuery) {
  687 + const deviceSearchQuery = searchQuery as DeviceSearchQuery;
  688 + deviceSearchQuery.deviceTypes = filter.deviceTypes;
  689 + findByQueryObservable = this.deviceService.findByQuery(deviceSearchQuery, true, true);
  690 + } else if (filter.type === AliasFilterType.entityViewSearchQuery) {
  691 + const entityViewSearchQuery = searchQuery as EntityViewSearchQuery;
  692 + entityViewSearchQuery.entityViewTypes = filter.entityViewTypes;
  693 + findByQueryObservable = this.entityViewService.findByQuery(entityViewSearchQuery, true, true);
  694 + }
  695 + return findByQueryObservable.pipe(
  696 + map((entities) => {
  697 + if (entities && entities.length || !failOnEmpty) {
  698 + if (isDefined(maxItems) && maxItems > 0 && entities) {
  699 + const limit = Math.min(entities.length, maxItems);
  700 + entities.length = limit;
  701 + }
  702 + result.entities = this.entitiesToEntitiesInfo(entities);
  703 + return result;
  704 + } else {
  705 + throw Error();
  706 + }
  707 + })
  708 + );
  709 + } else {
  710 + return of(result);
  711 + }
  712 + break;
  713 + }
  714 + }
  715 +
  716 + private entitiesToEntitiesInfo(entities: Array<BaseData<EntityId>>): Array<EntityInfo> {
  717 + const entitiesInfo = [];
  718 + if (entities) {
  719 + entities.forEach((entity) => {
  720 + entitiesInfo.push(this.entityToEntityInfo(entity));
  721 + });
  722 + }
  723 + return entitiesInfo;
  724 + }
  725 +
  726 + private entityToEntityInfo(entity: BaseData<EntityId>): EntityInfo {
  727 + return {
  728 + origEntity: entity,
  729 + name: entity.name,
  730 + label: (entity as any).label ? (entity as any).label : '',
  731 + entityType: entity.id.entityType as EntityType,
  732 + id: entity.id.id,
  733 + entityDescription: (entity as any).additionalInfo ? (entity as any).additionalInfo.description : ''
  734 + };
  735 + }
  736 +
  737 + private entityRelationInfosToEntitiesInfo(entityRelations: Array<EntityRelationInfo>,
  738 + direction: EntitySearchDirection): Observable<Array<EntityInfo>> {
  739 + if (entityRelations) {
  740 + const tasks: Observable<EntityInfo>[] = [];
  741 + entityRelations.forEach((entityRelation) => {
  742 + tasks.push(this.entityRelationInfoToEntityInfo(entityRelation, direction));
  743 + });
  744 + return forkJoin(tasks);
  745 + } else {
  746 + return of([]);
  747 + }
  748 + }
  749 +
  750 + private entityRelationInfoToEntityInfo(entityRelationInfo: EntityRelationInfo, direction: EntitySearchDirection): Observable<EntityInfo> {
  751 + const entityId = direction === EntitySearchDirection.FROM ? entityRelationInfo.to : entityRelationInfo.from;
  752 + return this.getEntity(entityId.entityType as EntityType, entityId.id, true, true).pipe(
  753 + map((entity) => {
  754 + return this.entityToEntityInfo(entity);
  755 + })
  756 + );
  757 + }
  758 +
  759 + private getStateEntityId(filter: EntityAliasFilter, stateParams: StateParams): EntityId {
  760 + let entityId = null;
  761 + if (stateParams) {
  762 + if (filter.stateEntityParamName && filter.stateEntityParamName.length) {
  763 + if (stateParams[filter.stateEntityParamName]) {
  764 + entityId = stateParams[filter.stateEntityParamName].entityId;
  765 + }
  766 + } else {
  767 + entityId = stateParams.entityId;
  768 + }
  769 + }
  770 + if (!entityId) {
  771 + entityId = filter.defaultStateEntity;
  772 + }
  773 + if (entityId) {
  774 + entityId = this.resolveAliasEntityId(entityId.entityType, entityId.id);
  775 + }
  776 + return entityId;
  777 + }
  778 +
  779 + private resolveAliasEntityId(entityType: EntityType | AliasEntityType, id: string): EntityId {
  780 + const entityId: EntityId = {
  781 + entityType,
  782 + id
  783 + };
  784 + if (entityType === AliasEntityType.CURRENT_CUSTOMER) {
  785 + const authUser = getCurrentAuthUser(this.store);
  786 + entityId.entityType = EntityType.CUSTOMER;
  787 + if (authUser.authority === Authority.CUSTOMER_USER) {
  788 + entityId.id = authUser.customerId;
  789 + }
  790 + }
  791 + return entityId;
  792 + }
  793 +
447 private createDatasourcesFromSubscriptionInfo(subscriptionInfo: SubscriptionInfo): Observable<Array<Datasource>> { 794 private createDatasourcesFromSubscriptionInfo(subscriptionInfo: SubscriptionInfo): Observable<Array<Datasource>> {
448 subscriptionInfo = this.validateSubscriptionInfo(subscriptionInfo); 795 subscriptionInfo = this.validateSubscriptionInfo(subscriptionInfo);
449 if (subscriptionInfo.type === DatasourceType.entity) { 796 if (subscriptionInfo.type === DatasourceType.entity) {
@@ -278,6 +278,17 @@ export class DashboardUtilsService { @@ -278,6 +278,17 @@ export class DashboardUtilsService {
278 } 278 }
279 } 279 }
280 280
  281 + public getWidgetsArray(dashboard: Dashboard): Array<Widget> {
  282 + const widgetsArray: Array<Widget> = [];
  283 + const dashboardConfiguration = dashboard.configuration;
  284 + const widgets = dashboardConfiguration.widgets;
  285 + for (const widgetId of Object.keys(widgets)) {
  286 + const widget = widgets[widgetId];
  287 + widgetsArray.push(widget);
  288 + }
  289 + return widgetsArray;
  290 + }
  291 +
281 private validateAndUpdateEntityAliases(configuration: DashboardConfiguration, 292 private validateAndUpdateEntityAliases(configuration: DashboardConfiguration,
282 datasourcesByAliasId: {[aliasId: string]: Array<Datasource>}, 293 datasourcesByAliasId: {[aliasId: string]: Array<Datasource>},
283 targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration { 294 targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration {
@@ -17,19 +17,39 @@ @@ -17,19 +17,39 @@
17 import { Inject, Injectable } from '@angular/core'; 17 import { Inject, Injectable } from '@angular/core';
18 import { WINDOW } from '@core/services/window.service'; 18 import { WINDOW } from '@core/services/window.service';
19 import { ExceptionData } from '@app/shared/models/error.models'; 19 import { ExceptionData } from '@app/shared/models/error.models';
20 -import { isUndefined, isDefined, deepClone } from '@core/utils'; 20 +import { deepClone, isDefined, isUndefined, deleteNullProperties } from '@core/utils';
21 import { WindowMessage } from '@shared/models/window-message.model'; 21 import { WindowMessage } from '@shared/models/window-message.model';
22 import { TranslateService } from '@ngx-translate/core'; 22 import { TranslateService } from '@ngx-translate/core';
23 import { customTranslationsPrefix } from '@app/shared/models/constants'; 23 import { customTranslationsPrefix } from '@app/shared/models/constants';
24 -import { DataKey, Datasource, DatasourceType, KeyInfo, Widget } from '@shared/models/widget.models'; 24 +import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models';
25 import { EntityType } from '@shared/models/entity-type.models'; 25 import { EntityType } from '@shared/models/entity-type.models';
26 import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models'; 26 import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models';
27 import { alarmFields } from '@shared/models/alarm.models'; 27 import { alarmFields } from '@shared/models/alarm.models';
28 import { materialColors } from '@app/shared/models/material.models'; 28 import { materialColors } from '@app/shared/models/material.models';
29 import { WidgetInfo } from '@home/models/widget-component.models'; 29 import { WidgetInfo } from '@home/models/widget-component.models';
  30 +import jsonSchemaDefaults from 'json-schema-defaults';
30 31
31 const varsRegex = /\$\{([^}]*)\}/g; 32 const varsRegex = /\$\{([^}]*)\}/g;
32 33
  34 +const predefinedFunctions: {[func: string]: string} = {
  35 + Sin: 'return Math.round(1000*Math.sin(time/5000));',
  36 + Cos: 'return Math.round(1000*Math.cos(time/5000));',
  37 + Random: 'var value = prevValue + Math.random() * 100 - 50;\n' +
  38 + 'var multiplier = Math.pow(10, 2 || 0);\n' +
  39 + 'var value = Math.round(value * multiplier) / multiplier;\n' +
  40 + 'if (value < -1000) {\n' +
  41 + ' value = -1000;\n' +
  42 + '} else if (value > 1000) {\n' +
  43 + ' value = 1000;\n' +
  44 + '}\n' +
  45 + 'return value;'
  46 +};
  47 +
  48 +const predefinedFunctionsList: Array<string> = [];
  49 +for (const func of Object.keys(predefinedFunctions)) {
  50 + predefinedFunctionsList.push(func);
  51 +}
  52 +
33 @Injectable({ 53 @Injectable({
34 providedIn: 'root' 54 providedIn: 'root'
35 }) 55 })
@@ -39,6 +59,22 @@ export class UtilsService { @@ -39,6 +59,22 @@ export class UtilsService {
39 widgetEditMode = false; 59 widgetEditMode = false;
40 editWidgetInfo: WidgetInfo = null; 60 editWidgetInfo: WidgetInfo = null;
41 61
  62 + defaultDataKey: DataKey = {
  63 + name: 'f(x)',
  64 + type: DataKeyType.function,
  65 + label: 'Sin',
  66 + color: this.getMaterialColor(0),
  67 + funcBody: this.getPredefinedFunctionBody('Sin'),
  68 + settings: {},
  69 + _hash: Math.random()
  70 + };
  71 +
  72 + defaultDatasource: Datasource = {
  73 + type: DatasourceType.function,
  74 + name: DatasourceType.function,
  75 + dataKeys: [deepClone(this.defaultDataKey)]
  76 + };
  77 +
42 constructor(@Inject(WINDOW) private window: Window, 78 constructor(@Inject(WINDOW) private window: Window,
43 private translate: TranslateService) { 79 private translate: TranslateService) {
44 let frame: Element = null; 80 let frame: Element = null;
@@ -57,6 +93,28 @@ export class UtilsService { @@ -57,6 +93,28 @@ export class UtilsService {
57 } 93 }
58 } 94 }
59 95
  96 + public getPredefinedFunctionsList(): Array<string> {
  97 + return predefinedFunctionsList;
  98 + }
  99 +
  100 + public getPredefinedFunctionBody(func: string): string {
  101 + return predefinedFunctions[func];
  102 + }
  103 +
  104 + public getDefaultDatasource(dataKeySchema: any): Datasource {
  105 + const datasource = deepClone(this.defaultDatasource);
  106 + if (isDefined(dataKeySchema)) {
  107 + datasource.dataKeys[0].settings = this.generateObjectFromJsonSchema(dataKeySchema);
  108 + }
  109 + return datasource;
  110 + }
  111 +
  112 + public generateObjectFromJsonSchema(schema: any): any {
  113 + const obj = jsonSchemaDefaults(schema);
  114 + deleteNullProperties(obj);
  115 + return obj;
  116 + }
  117 +
60 public hashCode(str: string): number { 118 public hashCode(str: string): number {
61 let hash = 0; 119 let hash = 0;
62 let i: number; 120 let i: number;
@@ -192,7 +250,7 @@ export class UtilsService { @@ -192,7 +250,7 @@ export class UtilsService {
192 return datasources; 250 return datasources;
193 } 251 }
194 252
195 - public getMaterialColor(index) { 253 + public getMaterialColor(index: number) {
196 const colorIndex = index % materialColors.length; 254 const colorIndex = index % materialColors.length;
197 return materialColors[colorIndex].value; 255 return materialColors[colorIndex].value;
198 } 256 }
@@ -103,6 +103,23 @@ export function isString(value: any): boolean { @@ -103,6 +103,23 @@ export function isString(value: any): boolean {
103 return typeof value === 'string'; 103 return typeof value === 'string';
104 } 104 }
105 105
  106 +export function deleteNullProperties(obj: any) {
  107 + if (isUndefined(obj) || obj == null) {
  108 + return;
  109 + }
  110 + Object.keys(obj).forEach((propName) => {
  111 + if (obj[propName] === null || isUndefined(obj[propName])) {
  112 + delete obj[propName];
  113 + } else if (isObject(obj[propName])) {
  114 + deleteNullProperties(obj[propName]);
  115 + } else if (obj[propName] instanceof Array) {
  116 + (obj[propName] as any[]).forEach((elem) => {
  117 + deleteNullProperties(elem);
  118 + });
  119 + }
  120 + });
  121 +}
  122 +
106 export function objToBase64(obj: any): string { 123 export function objToBase64(obj: any): string {
107 const json = JSON.stringify(obj); 124 const json = JSON.stringify(obj);
108 const encoded = utf8Encode(json); 125 const encoded = utf8Encode(json);
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form #entityAliasForm="ngForm" [formGroup]="entityAliasFormGroup" (ngSubmit)="save()" style="width: 600px;">
  19 + <mat-toolbar fxLayout="row" color="primary">
  20 + <h2>{{ (isAdd ? 'alias.add' : 'alias.edit') | translate }}</h2>
  21 + <span fxFlex></span>
  22 + <button mat-button mat-icon-button
  23 + (click)="cancel()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div mat-dialog-content>
  31 + <fieldset [disabled]="isLoading$ | async">
  32 + <div fxFlex fxLayout="column">
  33 + <div fxLayout="row">
  34 + <mat-form-field fxFlex class="mat-block">
  35 + <mat-label translate>alias.name</mat-label>
  36 + <input matInput formControlName="alias" required>
  37 + <mat-error *ngIf="entityAliasFormGroup.get('alias').hasError('required')">
  38 + {{ 'alias.name-required' | translate }}
  39 + </mat-error>
  40 + <mat-error *ngIf="entityAliasFormGroup.get('alias').hasError('duplicateAliasName')">
  41 + {{ 'alias.duplicate-alias' | translate }}
  42 + </mat-error>
  43 + </mat-form-field>
  44 + <section class="tb-resolve-multiple-switch" fxLayout="column" fxLayoutAlign="start center">
  45 + <label class="tb-small resolve-multiple-label" translate>alias.resolve-multiple</label>
  46 + <mat-slide-toggle class="resolve-multiple-switch"
  47 + formControlName="resolveMultiple">
  48 + </mat-slide-toggle>
  49 + </section>
  50 + </div>
  51 + <tb-entity-filter formControlName="filter"
  52 + [resolveMultiple]="entityAliasFormGroup.get('resolveMultiple').value"
  53 + (resolveMultipleChanged)="entityAliasFormGroup.get('resolveMultiple').patchValue($event)"
  54 + [allowedEntityTypes]="allowedEntityTypes">
  55 + </tb-entity-filter>
  56 + <tb-error [error]="entityAliasFormGroup.hasError('noEntityMatched')
  57 + ? translate.instant('alias.entity-filter-no-entity-matched') : ''"></tb-error>
  58 + </div>
  59 + </fieldset>
  60 + </div>
  61 + <div mat-dialog-actions fxLayout="row">
  62 + <span fxFlex></span>
  63 + <button mat-button mat-raised-button color="primary"
  64 + type="submit"
  65 + [disabled]="(isLoading$ | async) || entityAliasFormGroup.invalid || !entityAliasFormGroup.dirty">
  66 + {{ (isAdd ? 'action.add' : 'action.save') | translate }}
  67 + </button>
  68 + <button mat-button color="primary"
  69 + style="margin-right: 20px;"
  70 + type="button"
  71 + [disabled]="(isLoading$ | async)"
  72 + (click)="cancel()" cdkFocusInitial>
  73 + {{ 'action.cancel' | translate }}
  74 + </button>
  75 + </div>
  76 +</form>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .tb-resolve-multiple-switch {
  18 + padding-left: 10px;
  19 +
  20 + .resolve-multiple-switch {
  21 + margin: 0;
  22 + }
  23 +
  24 + .resolve-multiple-label {
  25 + margin: 5px 0;
  26 + }
  27 + }
  28 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
  18 +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import {
  22 + AbstractControl,
  23 + FormArray,
  24 + FormBuilder,
  25 + FormControl,
  26 + FormGroup,
  27 + FormGroupDirective,
  28 + NgForm, Validators, ValidatorFn
  29 +} from '@angular/forms';
  30 +import { Router } from '@angular/router';
  31 +import { DialogComponent } from '@app/shared/components/dialog.component';
  32 +import { AttributeData } from '@shared/models/telemetry/telemetry.models';
  33 +import { EntityAlias, EntityAliases, EntityAliasFilter } from '@shared/models/alias.models';
  34 +import { DatasourceType, Widget, widgetType } from '@shared/models/widget.models';
  35 +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
  36 +import { UtilsService } from '@core/services/utils.service';
  37 +import { TranslateService } from '@ngx-translate/core';
  38 +import { ActionNotificationShow } from '@core/notification/notification.actions';
  39 +import { DialogService } from '@core/services/dialog.service';
  40 +import { EntityService } from '@core/http/entity.service';
  41 +import { Observable, of } from 'rxjs';
  42 +
  43 +export interface EntityAliasDialogData {
  44 + isAdd: boolean;
  45 + allowedEntityTypes: Array<EntityType | AliasEntityType>;
  46 + entityAliases: EntityAliases | Array<EntityAlias>;
  47 + alias?: EntityAlias;
  48 +}
  49 +
  50 +@Component({
  51 + selector: 'tb-entity-alias-dialog',
  52 + templateUrl: './entity-alias-dialog.component.html',
  53 + providers: [{provide: ErrorStateMatcher, useExisting: EntityAliasDialogComponent}],
  54 + styleUrls: ['./entity-alias-dialog.component.scss']
  55 +})
  56 +export class EntityAliasDialogComponent extends DialogComponent<EntityAliasDialogComponent, EntityAlias>
  57 + implements OnInit, ErrorStateMatcher {
  58 +
  59 + isAdd: boolean;
  60 + allowedEntityTypes: Array<EntityType | AliasEntityType>;
  61 + entityAliases: Array<EntityAlias>;
  62 +
  63 + alias: EntityAlias;
  64 +
  65 + entityAliasFormGroup: FormGroup;
  66 +
  67 + submitted = false;
  68 +
  69 + constructor(protected store: Store<AppState>,
  70 + protected router: Router,
  71 + @Inject(MAT_DIALOG_DATA) public data: EntityAliasDialogData,
  72 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
  73 + public dialogRef: MatDialogRef<EntityAliasDialogComponent, EntityAlias>,
  74 + private fb: FormBuilder,
  75 + private utils: UtilsService,
  76 + private translate: TranslateService,
  77 + private entityService: EntityService) {
  78 + super(store, router, dialogRef);
  79 + this.isAdd = data.isAdd;
  80 + this.allowedEntityTypes = data.allowedEntityTypes;
  81 + if (Array.isArray(data.entityAliases)) {
  82 + this.entityAliases = data.entityAliases;
  83 + } else {
  84 + this.entityAliases = [];
  85 + for (const aliasId of Object.keys(data.entityAliases)) {
  86 + this.entityAliases.push(data.entityAliases[aliasId]);
  87 + }
  88 + }
  89 + if (this.isAdd) {
  90 + this.alias = {
  91 + id: null,
  92 + alias: '',
  93 + filter: {
  94 + resolveMultiple: false
  95 + }
  96 + };
  97 + } else {
  98 + this.alias = data.alias;
  99 + }
  100 +
  101 + this.entityAliasFormGroup = this.fb.group({
  102 + alias: [this.alias.alias, [this.validateDuplicateAliasName(), Validators.required]],
  103 + resolveMultiple: [this.alias.filter.resolveMultiple],
  104 + filter: [this.alias.filter, Validators.required]
  105 + });
  106 + }
  107 +
  108 + validateDuplicateAliasName(): ValidatorFn {
  109 + return (c: FormControl) => {
  110 + const newAlias = c.value;
  111 + const found = this.entityAliases.find((entityAlias) => entityAlias.alias === newAlias);
  112 + if (found) {
  113 + if (this.isAdd || this.alias.id !== found.id) {
  114 + return {
  115 + duplicateAliasName: {
  116 + valid: false
  117 + }
  118 + };
  119 + }
  120 + }
  121 + return null;
  122 + };
  123 + }
  124 +
  125 + ngOnInit(): void {
  126 + }
  127 +
  128 + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
  129 + const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
  130 + const customErrorState = !!(control && control.invalid && this.submitted);
  131 + return originalErrorState || customErrorState;
  132 + }
  133 +
  134 + cancel(): void {
  135 + this.dialogRef.close(null);
  136 + }
  137 +
  138 + private validate(): Observable<any> {
  139 + return this.entityService.resolveAliasFilter(this.alias.filter, null, 1, true);
  140 + }
  141 +
  142 + save(): void {
  143 + this.submitted = true;
  144 + this.alias.alias = this.entityAliasFormGroup.get('alias').value;
  145 + this.alias.filter = this.entityAliasFormGroup.get('filter').value;
  146 + this.alias.filter.resolveMultiple = this.entityAliasFormGroup.get('resolveMultiple').value;
  147 + this.validate().subscribe(() => {
  148 + if (this.isAdd) {
  149 + this.alias.id = this.utils.guid();
  150 + }
  151 + this.dialogRef.close(this.alias);
  152 + },
  153 + () => {
  154 + this.entityAliasFormGroup.setErrors({
  155 + noEntityMatched: true
  156 + });
  157 + const changesSubscriptuion = this.entityAliasFormGroup.valueChanges.subscribe(() => {
  158 + this.entityAliasFormGroup.setErrors(null);
  159 + changesSubscriptuion.unsubscribe();
  160 + });
  161 + }
  162 + );
  163 + }
  164 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form #entityAliasesForm="ngForm" [formGroup]="entityAliasesFormGroup" (ngSubmit)="save()" style="width: 700px;">
  19 + <mat-toolbar fxLayout="row" color="primary">
  20 + <h2>{{ title | translate }}</h2>
  21 + <span fxFlex></span>
  22 + <button mat-button mat-icon-button
  23 + (click)="cancel()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  31 + <div class="tb-aliases-header" fxFlex fxLayout="row" fxLayoutAlign="start center">
  32 + <span fxFlex="5"></span>
  33 + <div fxFlex="95" fxLayout="row" fxLayoutAlign="start center">
  34 + <span class="tb-header-label" translate fxFlex="150px">alias.name</span>
  35 + <span class="tb-header-label" translate fxFlex style="padding-left: 10px;">alias.entity-filter</span>
  36 + <span class="tb-header-label" translate fxFlex="120px" style="padding-left: 10px;">alias.resolve-multiple</span>
  37 + <span style="min-width: 80px;"></span>
  38 + </div>
  39 + </div>
  40 + <mat-divider></mat-divider>
  41 + <div mat-dialog-content>
  42 + <fieldset [disabled]="isLoading$ | async">
  43 + <div fxFlex fxLayout="row" fxLayoutAlign="start center"
  44 + formArrayName="entityAliases"
  45 + *ngFor="let entityAliasControl of entityAliasesFormGroup.get('entityAliases').controls; let $index = index">
  46 + <span fxFlex="5">{{$index + 1}}.</span>
  47 + <div class="mat-elevation-z4 tb-alias" fxFlex="95" fxLayout="row" fxLayoutAlign="start center">
  48 + <mat-form-field floatLabel="always" hideRequiredMarker class="mat-block" fxFlex="150px">
  49 + <mat-label></mat-label>
  50 + <input matInput [formControl]="entityAliasControl.get('alias')" required placeholder="{{ 'entity.alias' | translate }}">
  51 + <mat-error *ngIf="entityAliasControl.get('alias').hasError('required')">
  52 + {{ 'entity.alias-required' | translate }}
  53 + </mat-error>
  54 + </mat-form-field>
  55 + <tb-entity-filter-view fxFlex style="padding-left: 10px;" [formControl]="entityAliasControl.get('filter')">
  56 + </tb-entity-filter-view>
  57 + <section fxFlex="120px" style="padding-left: 10px;"
  58 + class="tb-resolve-multiple-switch"
  59 + fxLayout="column"
  60 + fxLayoutAlign="center center">
  61 + <mat-slide-toggle class="resolve-multiple-switch"
  62 + [formControl]="entityAliasControl.get('resolveMultiple')">
  63 + </mat-slide-toggle>
  64 + </section>
  65 + <button [disabled]="isLoading$ | async"
  66 + mat-button mat-icon-button color="primary"
  67 + style="min-width: 40px;"
  68 + type="button"
  69 + (click)="editAlias($index)"
  70 + matTooltip="{{ 'alias.edit' | translate }}"
  71 + matTooltipPosition="above">
  72 + <mat-icon>edit</mat-icon>
  73 + </button>
  74 + <button [disabled]="isLoading$ | async"
  75 + mat-button mat-icon-button color="primary"
  76 + style="min-width: 40px;"
  77 + type="button"
  78 + (click)="removeAlias($index)"
  79 + matTooltip="{{ 'entity.remove-alias' | translate }}"
  80 + matTooltipPosition="above">
  81 + <mat-icon>close</mat-icon>
  82 + </button>
  83 + </div>
  84 + </div>
  85 + </fieldset>
  86 + </div>
  87 + <div mat-dialog-actions fxLayout="row">
  88 + <button mat-button mat-raised-button color="primary"
  89 + type="button"
  90 + (click)="addAlias()"
  91 + [fxShow]="!disableAdd"
  92 + [disabled]="isLoading$ | async"
  93 + matTooltip="{{ 'alias.add' | translate }}"
  94 + matTooltipPosition="above">
  95 + {{ 'alias.add' | translate }}
  96 + </button>
  97 + <span fxFlex></span>
  98 + <button mat-button mat-raised-button color="primary"
  99 + type="submit"
  100 + [disabled]="(isLoading$ | async) || entityAliasesFormGroup.invalid || !entityAliasesFormGroup.dirty">
  101 + {{ 'action.save' | translate }}
  102 + </button>
  103 + <button mat-button color="primary"
  104 + style="margin-right: 20px;"
  105 + type="button"
  106 + [disabled]="(isLoading$ | async)"
  107 + (click)="cancel()" cdkFocusInitial>
  108 + {{ 'action.cancel' | translate }}
  109 + </button>
  110 + </div>
  111 +</form>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +:host {
  18 +
  19 + .tb-aliases-header {
  20 + min-height: 40px;
  21 + padding: 0 34px 0 34px;
  22 + margin: 5px;
  23 +
  24 + .tb-header-label {
  25 + font-size: 14px;
  26 + color: rgba(0, 0, 0, .570588);
  27 + }
  28 + }
  29 +
  30 + .tb-alias {
  31 + padding: 0 0 0 10px;
  32 + margin: 5px;
  33 +
  34 + .tb-resolve-multiple-switch {
  35 + padding-left: 10px;
  36 +
  37 + .resolve-multiple-switch {
  38 + margin: 0;
  39 + }
  40 + }
  41 + }
  42 +}
  43 +
  44 +:host ::ng-deep {
  45 + .mat-dialog-content {
  46 + padding-top: 0 !important;
  47 + padding-bottom: 0 !important;
  48 + }
  49 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
  18 +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import {
  22 + AbstractControl,
  23 + FormArray,
  24 + FormBuilder,
  25 + FormControl,
  26 + FormGroup,
  27 + FormGroupDirective,
  28 + NgForm, Validators
  29 +} from '@angular/forms';
  30 +import { Router } from '@angular/router';
  31 +import { DialogComponent } from '@app/shared/components/dialog.component';
  32 +import { AttributeData } from '@shared/models/telemetry/telemetry.models';
  33 +import { EntityAlias, EntityAliases, EntityAliasFilter } from '@shared/models/alias.models';
  34 +import { DatasourceType, Widget, widgetType } from '@shared/models/widget.models';
  35 +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
  36 +import { UtilsService } from '@core/services/utils.service';
  37 +import { TranslateService } from '@ngx-translate/core';
  38 +import { ActionNotificationShow } from '@core/notification/notification.actions';
  39 +import { DialogService } from '@core/services/dialog.service';
  40 +import { deepClone } from '@core/utils';
  41 +import { MatDialog } from '@angular/material/dialog';
  42 +import { EntityAliasDialogComponent, EntityAliasDialogData } from './entity-alias-dialog.component';
  43 +
  44 +export interface EntityAliasesDialogData {
  45 + entityAliases: EntityAliases;
  46 + widgets: Array<Widget>;
  47 + isSingleEntityAlias: boolean;
  48 + isSingleWidget?: boolean;
  49 + allowedEntityTypes?: Array<AliasEntityType>;
  50 + disableAdd?: boolean;
  51 + singleEntityAlias?: EntityAlias;
  52 + customTitle?: string;
  53 +}
  54 +
  55 +@Component({
  56 + selector: 'tb-entity-aliases-dialog',
  57 + templateUrl: './entity-aliases-dialog.component.html',
  58 + providers: [{provide: ErrorStateMatcher, useExisting: EntityAliasesDialogComponent}],
  59 + styleUrls: ['./entity-aliases-dialog.component.scss']
  60 +})
  61 +export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesDialogComponent, EntityAliases>
  62 + implements OnInit, ErrorStateMatcher {
  63 +
  64 + title: string;
  65 + disableAdd: boolean;
  66 + allowedEntityTypes: Array<EntityType | AliasEntityType>;
  67 +
  68 + aliasToWidgetsMap: {[aliasId: string]: Array<string>} = {};
  69 +
  70 + entityAliasesFormGroup: FormGroup;
  71 +
  72 + submitted = false;
  73 +
  74 + constructor(protected store: Store<AppState>,
  75 + protected router: Router,
  76 + @Inject(MAT_DIALOG_DATA) public data: EntityAliasesDialogData,
  77 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
  78 + public dialogRef: MatDialogRef<EntityAliasesDialogComponent, EntityAliases>,
  79 + private fb: FormBuilder,
  80 + private utils: UtilsService,
  81 + private translate: TranslateService,
  82 + private dialogs: DialogService,
  83 + private dialog: MatDialog) {
  84 + super(store, router, dialogRef);
  85 + this.title = data.customTitle ? data.customTitle : 'entity.aliases';
  86 + this.disableAdd = this.data.disableAdd;
  87 + this.allowedEntityTypes = this.data.allowedEntityTypes;
  88 +
  89 + if (data.widgets) {
  90 + let widgetsTitleList: Array<string>;
  91 + if (this.data.isSingleWidget && this.data.widgets.length === 1) {
  92 + const widget = this.data.widgets[0];
  93 + widgetsTitleList = [widget.config.title];
  94 + for (const aliasId of Object.keys(this.data.entityAliases)) {
  95 + this.aliasToWidgetsMap[aliasId] = widgetsTitleList;
  96 + }
  97 + } else {
  98 + this.data.widgets.forEach((widget) => {
  99 + if (widget.type === widgetType.rpc) {
  100 + if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
  101 + const targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
  102 + widgetsTitleList = this.aliasToWidgetsMap[targetDeviceAliasId];
  103 + if (!widgetsTitleList) {
  104 + widgetsTitleList = [];
  105 + this.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList;
  106 + }
  107 + widgetsTitleList.push(widget.config.title);
  108 + }
  109 + } else {
  110 + const datasources = this.utils.validateDatasources(widget.config.datasources);
  111 + datasources.forEach((datasource) => {
  112 + if (datasource.type === DatasourceType.entity && datasource.entityAliasId) {
  113 + widgetsTitleList = this.aliasToWidgetsMap[datasource.entityAliasId];
  114 + if (!widgetsTitleList) {
  115 + widgetsTitleList = [];
  116 + this.aliasToWidgetsMap[datasource.entityAliasId] = widgetsTitleList;
  117 + }
  118 + widgetsTitleList.push(widget.config.title);
  119 + }
  120 + });
  121 + }
  122 + });
  123 + }
  124 + }
  125 + const entityAliasControls: Array<AbstractControl> = [];
  126 + for (const aliasId of Object.keys(this.data.entityAliases)) {
  127 + const entityAlias = this.data.entityAliases[aliasId];
  128 + let filter = entityAlias.filter;
  129 + if (!filter) {
  130 + filter = {
  131 + resolveMultiple: false
  132 + };
  133 + }
  134 + if (!filter.resolveMultiple) {
  135 + filter.resolveMultiple = false;
  136 + }
  137 + entityAliasControls.push(this.createEntityAliasFormControl(aliasId, entityAlias));
  138 + }
  139 +
  140 + this.entityAliasesFormGroup = this.fb.group({
  141 + entityAliases: this.fb.array(entityAliasControls)
  142 + });
  143 + }
  144 +
  145 + private createEntityAliasFormControl(aliasId: string, entityAlias: EntityAlias): AbstractControl {
  146 + const aliasFormControl = this.fb.group({
  147 + id: [aliasId],
  148 + alias: [entityAlias ? entityAlias.alias : null, [Validators.required]],
  149 + filter: [entityAlias ? entityAlias.filter : null],
  150 + resolveMultiple: [entityAlias ? entityAlias.filter.resolveMultiple : false]
  151 + });
  152 + aliasFormControl.get('resolveMultiple').valueChanges.subscribe((resolveMultiple: boolean) => {
  153 + (aliasFormControl.get('filter').value as EntityAliasFilter).resolveMultiple = resolveMultiple;
  154 + });
  155 + return aliasFormControl;
  156 + }
  157 +
  158 + ngOnInit(): void {
  159 + }
  160 +
  161 + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
  162 + const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
  163 + const customErrorState = !!(control && control.invalid && this.submitted);
  164 + return originalErrorState || customErrorState;
  165 + }
  166 +
  167 + removeAlias(index: number) {
  168 + const entityAlias = (this.entityAliasesFormGroup.get('entityAliases').value as any[])[index];
  169 + const widgetsTitleList = this.aliasToWidgetsMap[entityAlias.id];
  170 + if (widgetsTitleList) {
  171 + let widgetsListHtml = '';
  172 + for (const widgetTitle of widgetsTitleList) {
  173 + widgetsListHtml += '<br/>\'' + widgetTitle + '\'';
  174 + }
  175 + const message = this.translate.instant('entity.unable-delete-entity-alias-text',
  176 + {entityAlias: entityAlias.alias, widgetsList: widgetsListHtml});
  177 + this.dialogs.alert(this.translate.instant('entity.unable-delete-entity-alias-title'),
  178 + message, this.translate.instant('action.close'), true);
  179 + } else {
  180 + (this.entityAliasesFormGroup.get('entityAliases') as FormArray).removeAt(index);
  181 + }
  182 + }
  183 +
  184 + public addAlias() {
  185 + this.openAliasDialog(-1);
  186 + }
  187 +
  188 + public editAlias(index: number) {
  189 + this.openAliasDialog(index);
  190 + }
  191 +
  192 + private openAliasDialog(index: number) {
  193 + const isAdd = index === -1;
  194 + let alias;
  195 + const aliasesArray = this.entityAliasesFormGroup.get('entityAliases').value as any[];
  196 + if (!isAdd) {
  197 + alias = aliasesArray[index];
  198 + }
  199 + this.dialog.open<EntityAliasDialogComponent, EntityAliasDialogData,
  200 + EntityAlias>(EntityAliasDialogComponent, {
  201 + disableClose: true,
  202 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  203 + data: {
  204 + isAdd,
  205 + allowedEntityTypes: this.allowedEntityTypes,
  206 + entityAliases: aliasesArray,
  207 + alias: isAdd ? null : deepClone(alias)
  208 + }
  209 + }).afterClosed().subscribe((entityAlias) => {
  210 + if (entityAlias) {
  211 + if (isAdd) {
  212 + (this.entityAliasesFormGroup.get('entityAliases') as FormArray)
  213 + .push(this.createEntityAliasFormControl(entityAlias.id, entityAlias));
  214 + } else {
  215 + const aliasFormControl = (this.entityAliasesFormGroup.get('entityAliases') as FormArray).at(index);
  216 + aliasFormControl.get('alias').patchValue(entityAlias.alias);
  217 + aliasFormControl.get('filter').patchValue(entityAlias.filter);
  218 + aliasFormControl.get('resolveMultiple').patchValue(entityAlias.filter.resolveMultiple);
  219 + }
  220 + }
  221 + });
  222 + }
  223 +
  224 + cancel(): void {
  225 + this.dialogRef.close(null);
  226 + }
  227 +
  228 + save(): void {
  229 + this.submitted = true;
  230 + const entityAliases: EntityAliases = {};
  231 + const uniqueAliasList: {[alias: string]: string} = {};
  232 +
  233 + let valid = true;
  234 + let message: string;
  235 +
  236 + const aliasesArray = this.entityAliasesFormGroup.get('entityAliases').value as any[];
  237 + for (const aliasValue of aliasesArray) {
  238 + const aliasId: string = aliasValue.id;
  239 + const alias: string = aliasValue.alias;
  240 + const filter: EntityAliasFilter = aliasValue.filter;
  241 + if (uniqueAliasList[alias]) {
  242 + valid = false;
  243 + message = this.translate.instant('entity.duplicate-alias-error', {alias});
  244 + break;
  245 + } else if (!filter || !filter.type) {
  246 + valid = false;
  247 + message = this.translate.instant('entity.missing-entity-filter-error', {alias});
  248 + break;
  249 + } else {
  250 + uniqueAliasList[alias] = alias;
  251 + entityAliases[aliasId] = {id: aliasId, alias, filter};
  252 + }
  253 + }
  254 + if (valid) {
  255 + this.dialogRef.close(entityAliases);
  256 + } else {
  257 + this.store.dispatch(new ActionNotificationShow(
  258 + {
  259 + message,
  260 + type: 'error'
  261 + }));
  262 + }
  263 + }
  264 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div fxLayout="column" class="tb-entity-filter-view">
  19 + <div *ngIf="!filter || !filter.type; else filterView" class="entity-filter-empty" translate>alias.no-entity-filter-specified</div>
  20 + <ng-template #filterView>
  21 + <div fxLayout="column">
  22 + <div class="entity-filter-value">{{ filterDisplayValue }}</div>
  23 + </div>
  24 + </ng-template>
  25 +</div>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .tb-entity-filter-view {
  18 + .entity-filter-empty {
  19 + font-size: 14px;
  20 + line-height: 16px;
  21 + color: rgba(221, 44, 0, .87);
  22 + }
  23 +
  24 + .entity-filter-type {
  25 + font-size: 14px;
  26 + line-height: 16px;
  27 + color: rgba(0, 0, 0, .570588);
  28 + }
  29 +
  30 + .entity-filter-value {
  31 + font-size: 14px;
  32 + line-height: 16px;
  33 + color: rgba(0, 0, 0, .570588);
  34 + }
  35 + }
  36 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, forwardRef } from '@angular/core';
  18 +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
  19 +import { AliasFilterType, EntityAliasFilter } from '@shared/models/alias.models';
  20 +import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
  21 +import { TranslateService } from '@ngx-translate/core';
  22 +
  23 +@Component({
  24 + selector: 'tb-entity-filter-view',
  25 + templateUrl: './entity-filter-view.component.html',
  26 + styleUrls: ['./entity-filter-view.component.scss'],
  27 + providers: [
  28 + {
  29 + provide: NG_VALUE_ACCESSOR,
  30 + useExisting: forwardRef(() => EntityFilterViewComponent),
  31 + multi: true
  32 + }
  33 + ]
  34 +})
  35 +export class EntityFilterViewComponent implements ControlValueAccessor {
  36 +
  37 + constructor(private translate: TranslateService) {}
  38 +
  39 + filterDisplayValue: string;
  40 + filter: EntityAliasFilter;
  41 +
  42 + registerOnChange(fn: any): void {
  43 + }
  44 +
  45 + registerOnTouched(fn: any): void {
  46 + }
  47 +
  48 + setDisabledState?(isDisabled: boolean): void {
  49 + }
  50 +
  51 + writeValue(filter: EntityAliasFilter): void {
  52 + this.filter = filter;
  53 + if (this.filter && this.filter.type) {
  54 + let entityType: EntityType | AliasEntityType;
  55 + let prefix: string;
  56 + let allEntitiesText;
  57 + let anyRelationText;
  58 + let relationTypeText;
  59 + let rootEntityText;
  60 + let directionText;
  61 + switch (this.filter.type) {
  62 + case AliasFilterType.singleEntity:
  63 + entityType = this.filter.singleEntity.entityType;
  64 + this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).list,
  65 + {count: 1});
  66 + break;
  67 + case AliasFilterType.entityList:
  68 + entityType = this.filter.entityType;
  69 + const count = this.filter.entityList.length;
  70 + this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).list,
  71 + {count});
  72 + break;
  73 + case AliasFilterType.entityName:
  74 + entityType = this.filter.entityType;
  75 + prefix = this.filter.entityNameFilter;
  76 + this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).nameStartsWith,
  77 + {prefix});
  78 + break;
  79 + case AliasFilterType.stateEntity:
  80 + this.filterDisplayValue = this.translate.instant('alias.filter-type-state-entity-description');
  81 + break;
  82 + case AliasFilterType.assetType:
  83 + const assetType = this.filter.assetType;
  84 + prefix = this.filter.assetNameFilter;
  85 + if (prefix && prefix.length) {
  86 + this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-type-and-name-description',
  87 + {assetType, prefix});
  88 + } else {
  89 + this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-type-description',
  90 + {assetType});
  91 + }
  92 + break;
  93 + case AliasFilterType.deviceType:
  94 + const deviceType = this.filter.deviceType;
  95 + prefix = this.filter.deviceNameFilter;
  96 + if (prefix && prefix.length) {
  97 + this.filterDisplayValue = this.translate.instant('alias.filter-type-device-type-and-name-description',
  98 + {deviceType, prefix});
  99 + } else {
  100 + this.filterDisplayValue = this.translate.instant('alias.filter-type-device-type-description',
  101 + {deviceType});
  102 + }
  103 + break;
  104 + case AliasFilterType.entityViewType:
  105 + const entityViewType = this.filter.entityViewType;
  106 + prefix = this.filter.entityViewNameFilter;
  107 + if (prefix && prefix.length) {
  108 + this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-type-and-name-description',
  109 + {entityViewType, prefix});
  110 + } else {
  111 + this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-type-description',
  112 + {entityViewType});
  113 + }
  114 + break;
  115 + case AliasFilterType.relationsQuery:
  116 + allEntitiesText = this.translate.instant('alias.all-entities');
  117 + anyRelationText = this.translate.instant('alias.any-relation');
  118 + if (this.filter.rootStateEntity) {
  119 + rootEntityText = this.translate.instant('alias.state-entity');
  120 + } else {
  121 + rootEntityText = this.translate.instant(entityTypeTranslations.get(this.filter.rootEntity.entityType).type);
  122 + }
  123 + directionText = this.translate.instant('relation.direction-type.' + this.filter.direction);
  124 + const relationFilters = this.filter.filters;
  125 + if (relationFilters && relationFilters.length) {
  126 + const relationFiltersDisplayValues = [];
  127 + relationFilters.forEach((relationFilter) => {
  128 + let entitiesText;
  129 + if (relationFilter.entityTypes && relationFilter.entityTypes.length) {
  130 + const entitiesNamesList = [];
  131 + relationFilter.entityTypes.forEach((filterEntityType) => {
  132 + entitiesNamesList.push(
  133 + this.translate.instant(entityTypeTranslations.get(filterEntityType).typePlural)
  134 + );
  135 + });
  136 + entitiesText = entitiesNamesList.join(', ');
  137 + } else {
  138 + entitiesText = allEntitiesText;
  139 + }
  140 + if (relationFilter.relationType && relationFilter.relationType.length) {
  141 + relationTypeText = `'${relationFilter.relationType}'`;
  142 + } else {
  143 + relationTypeText = anyRelationText;
  144 + }
  145 + const relationFilterDisplayValue = this.translate.instant('alias.filter-type-relations-query-description',
  146 + {
  147 + entities: entitiesText,
  148 + relationType: relationTypeText,
  149 + direction: directionText,
  150 + rootEntity: rootEntityText
  151 + }
  152 + );
  153 + relationFiltersDisplayValues.push(relationFilterDisplayValue);
  154 + });
  155 + this.filterDisplayValue = relationFiltersDisplayValues.join(', ');
  156 + } else {
  157 + this.filterDisplayValue = this.translate.instant('alias.filter-type-relations-query-description',
  158 + {
  159 + entities: allEntitiesText,
  160 + relationType: anyRelationText,
  161 + direction: directionText,
  162 + rootEntity: rootEntityText
  163 + }
  164 + );
  165 + }
  166 + break;
  167 + case AliasFilterType.assetSearchQuery:
  168 + case AliasFilterType.deviceSearchQuery:
  169 + case AliasFilterType.entityViewSearchQuery:
  170 + allEntitiesText = this.translate.instant('alias.all-entities');
  171 + anyRelationText = this.translate.instant('alias.any-relation');
  172 + if (this.filter.rootStateEntity) {
  173 + rootEntityText = this.translate.instant('alias.state-entity');
  174 + } else {
  175 + rootEntityText = this.translate.instant(entityTypeTranslations.get(this.filter.rootEntity.entityType).type);
  176 + }
  177 + directionText = this.translate.instant('relation.direction-type.' + this.filter.direction);
  178 + if (this.filter.relationType && this.filter.relationType.length) {
  179 + relationTypeText = `'${filter.relationType}'`;
  180 + } else {
  181 + relationTypeText = anyRelationText;
  182 + }
  183 +
  184 + const translationValues: any = {
  185 + relationType: relationTypeText,
  186 + direction: directionText,
  187 + rootEntity: rootEntityText
  188 + };
  189 +
  190 + if (this.filter.type === AliasFilterType.assetSearchQuery) {
  191 + const assetTypesQuoted = [];
  192 + this.filter.assetTypes.forEach((filterAssetType) => {
  193 + assetTypesQuoted.push(`'${filterAssetType}'`);
  194 + });
  195 + const assetTypesText = assetTypesQuoted.join(', ');
  196 + translationValues.assetTypes = assetTypesText;
  197 + this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-search-query-description',
  198 + translationValues
  199 + );
  200 + } else if (this.filter.type === AliasFilterType.deviceSearchQuery) {
  201 + const deviceTypesQuoted = [];
  202 + this.filter.deviceTypes.forEach((filterDeviceType) => {
  203 + deviceTypesQuoted.push(`'${filterDeviceType}'`);
  204 + });
  205 + const deviceTypesText = deviceTypesQuoted.join(', ');
  206 + translationValues.deviceTypes = deviceTypesText;
  207 + this.filterDisplayValue = this.translate.instant('alias.filter-type-device-search-query-description',
  208 + translationValues
  209 + );
  210 + } else if (this.filter.type === AliasFilterType.entityViewSearchQuery) {
  211 + const entityViewTypesQuoted = [];
  212 + this.filter.entityViewTypes.forEach((filterEntityViewType) => {
  213 + entityViewTypesQuoted.push(`'${filterEntityViewType}'`);
  214 + });
  215 + const entityViewTypesText = entityViewTypesQuoted.join(', ');
  216 + translationValues.entityViewTypes = entityViewTypesText;
  217 + this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-search-query-description',
  218 + translationValues
  219 + );
  220 + }
  221 + break;
  222 + default:
  223 + this.filterDisplayValue = this.filter.type;
  224 + break;
  225 + }
  226 + } else {
  227 + this.filterDisplayValue = '';
  228 + }
  229 + }
  230 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div fxLayout="column" [formGroup]="entityFilterFormGroup" class="tb-entity-filter">
  19 + <mat-form-field class="mat-block">
  20 + <mat-label translate>alias.filter-type</mat-label>
  21 + <mat-select required matInput formControlName="type">
  22 + <mat-option *ngFor="let type of aliasFilterTypes" [value]="type">
  23 + {{aliasFilterTypeTranslations.get(type) | translate}}
  24 + </mat-option>
  25 + </mat-select>
  26 + <mat-error *ngIf="entityFilterFormGroup.get('type').hasError('required')">
  27 + {{ 'alias.filter-type-required' | translate }}
  28 + </mat-error>
  29 + </mat-form-field>
  30 + <section fxLayout="column" [formGroup]="filterFormGroup" [ngSwitch]="entityFilterFormGroup.get('type').value">
  31 + <ng-template [ngSwitchCase]="aliasFilterType.singleEntity">
  32 + <tb-entity-select required
  33 + useAliasEntityTypes="true"
  34 + formControlName="singleEntity">
  35 + </tb-entity-select>
  36 + </ng-template>
  37 + <ng-template [ngSwitchCase]="aliasFilterType.entityList">
  38 + <tb-entity-type-select required
  39 + showLabel
  40 + [allowedEntityTypes]="allowedEntityTypes"
  41 + formControlName="entityType">
  42 + </tb-entity-type-select>
  43 + <tb-entity-list required
  44 + [entityType]="filterFormGroup.get('entityType').value"
  45 + formControlName="entityList">
  46 + </tb-entity-list>
  47 + </ng-template>
  48 + <ng-template [ngSwitchCase]="aliasFilterType.entityName">
  49 + <tb-entity-type-select required
  50 + showLabel
  51 + [allowedEntityTypes]="allowedEntityTypes"
  52 + formControlName="entityType">
  53 + </tb-entity-type-select>
  54 + <mat-form-field class="mat-block">
  55 + <mat-label translate>entity.name-starts-with</mat-label>
  56 + <input matInput formControlName="entityNameFilter" required>
  57 + <mat-error *ngIf="filterFormGroup.get('entityNameFilter').hasError('required')">
  58 + {{ 'entity.entity-name-filter-required' | translate }}
  59 + </mat-error>
  60 + </mat-form-field>
  61 + </ng-template>
  62 + <ng-template [ngSwitchCase]="aliasFilterType.stateEntity">
  63 + <mat-form-field floatLabel="always" class="mat-block">
  64 + <mat-label translate>alias.state-entity-parameter-name</mat-label>
  65 + <input matInput formControlName="stateEntityParamName"
  66 + placeholder="{{ 'alias.default-entity-parameter-name' | translate }}">
  67 + </mat-form-field>
  68 + <div fxLayout="column">
  69 + <label class="tb-small">{{ 'alias.default-state-entity' | translate }}</label>
  70 + <tb-entity-select fxFlex
  71 + useAliasEntityTypes="true"
  72 + formControlName="defaultStateEntity">
  73 + </tb-entity-select>
  74 + </div>
  75 + </ng-template>
  76 + <ng-template [ngSwitchCase]="aliasFilterType.assetType">
  77 + <tb-entity-subtype-autocomplete required
  78 + formControlName="assetType"
  79 + [entityType]="entityType.ASSET">
  80 + </tb-entity-subtype-autocomplete>
  81 + <mat-form-field class="mat-block">
  82 + <mat-label translate>asset.name-starts-with</mat-label>
  83 + <input matInput formControlName="assetNameFilter">
  84 + </mat-form-field>
  85 + </ng-template>
  86 + <ng-template [ngSwitchCase]="aliasFilterType.deviceType">
  87 + <tb-entity-subtype-autocomplete required
  88 + formControlName="deviceType"
  89 + [entityType]="entityType.DEVICE">
  90 + </tb-entity-subtype-autocomplete>
  91 + <mat-form-field class="mat-block">
  92 + <mat-label translate>device.name-starts-with</mat-label>
  93 + <input matInput formControlName="deviceNameFilter">
  94 + </mat-form-field>
  95 + </ng-template>
  96 + <ng-template [ngSwitchCase]="aliasFilterType.entityViewType">
  97 + <tb-entity-subtype-autocomplete required
  98 + formControlName="entityViewType"
  99 + [entityType]="entityType.ENTITY_VIEW">
  100 + </tb-entity-subtype-autocomplete>
  101 + <mat-form-field class="mat-block">
  102 + <mat-label translate>entity-view.name-starts-with</mat-label>
  103 + <input matInput formControlName="entityViewNameFilter">
  104 + </mat-form-field>
  105 + </ng-template>
  106 + <ng-template [ngSwitchCase]="aliasFilterType.relationsQuery">
  107 + <section fxLayout="column" id="relationsQueryFilter">
  108 + <label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
  109 + <section class="tb-root-state-entity-switch" fxLayout="row" fxLayoutAlign="start center" style="padding-left: 0px;">
  110 + <mat-slide-toggle class="root-state-entity-switch"
  111 + formControlName="rootStateEntity">
  112 + </mat-slide-toggle>
  113 + <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
  114 + </section>
  115 + <div fxFlex fxLayout="row" *ngIf="!filterFormGroup.get('rootStateEntity').value">
  116 + <tb-entity-select fxFlex
  117 + required
  118 + useAliasEntityTypes="true"
  119 + formControlName="rootEntity">
  120 + </tb-entity-select>
  121 + </div>
  122 + <div fxFlex fxLayout="row" *ngIf="filterFormGroup.get('rootStateEntity').value">
  123 + <mat-form-field floatLabel="always" class="mat-block" style="margin-top: 14px; padding-right: 8px;">
  124 + <mat-label translate>alias.state-entity-parameter-name</mat-label>
  125 + <input matInput formControlName="stateEntityParamName"
  126 + placeholder="{{ 'alias.default-entity-parameter-name' | translate }}">
  127 + </mat-form-field>
  128 + <div fxFlex fxLayout="column">
  129 + <label class="tb-small">{{ 'alias.default-state-entity' | translate }}</label>
  130 + <tb-entity-select fxFlex
  131 + useAliasEntityTypes="true"
  132 + formControlName="defaultStateEntity">
  133 + </tb-entity-select>
  134 + </div>
  135 + </div>
  136 + <div fxFlex fxLayoutGap="8px" fxLayout="row">
  137 + <mat-form-field class="mat-block" style="min-width: 100px;">
  138 + <mat-label translate>relation.direction</mat-label>
  139 + <mat-select required matInput formControlName="direction">
  140 + <mat-option *ngFor="let type of directionTypes" [value]="type">
  141 + {{ directionTypeTranslations.get(type) | translate }}
  142 + </mat-option>
  143 + </mat-select>
  144 + </mat-form-field>
  145 + <mat-form-field fxFlex floatLabel="always" class="mat-block">
  146 + <mat-label translate>alias.max-relation-level</mat-label>
  147 + <input matInput
  148 + type="number"
  149 + min="1"
  150 + step="1"
  151 + placeholder="{{ 'alias.unlimited-level' | translate }}"
  152 + formControlName="maxLevel">
  153 + </mat-form-field>
  154 + </div>
  155 + <div class="mat-caption" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>relation.relation-filters</div>
  156 + <tb-relation-filters
  157 + [allowedEntityTypes]="allowedEntityTypes"
  158 + formControlName="filters">
  159 + </tb-relation-filters>
  160 + </section>
  161 + </ng-template>
  162 + <ng-template [ngSwitchCase]="entityFilterFormGroup.get('type').value === aliasFilterType.assetSearchQuery ||
  163 + entityFilterFormGroup.get('type').value === aliasFilterType.deviceSearchQuery ||
  164 + entityFilterFormGroup.get('type').value === aliasFilterType.entityViewSearchQuery ?
  165 + entityFilterFormGroup.get('type').value : ''">
  166 + <label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
  167 + <section class="tb-root-state-entity-switch" fxLayout="row" fxLayoutAlign="start center" style="padding-left: 0px;">
  168 + <mat-slide-toggle class="root-state-entity-switch"
  169 + formControlName="rootStateEntity">
  170 + </mat-slide-toggle>
  171 + <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
  172 + </section>
  173 + <div fxFlex fxLayout="row" *ngIf="!filterFormGroup.get('rootStateEntity').value">
  174 + <tb-entity-select fxFlex
  175 + required
  176 + useAliasEntityTypes="true"
  177 + formControlName="rootEntity">
  178 + </tb-entity-select>
  179 + </div>
  180 + <div fxFlex fxLayout="row" *ngIf="filterFormGroup.get('rootStateEntity').value">
  181 + <mat-form-field floatLabel="always" class="mat-block" style="margin-top: 14px; padding-right: 8px;">
  182 + <mat-label translate>alias.state-entity-parameter-name</mat-label>
  183 + <input matInput formControlName="stateEntityParamName"
  184 + placeholder="{{ 'alias.default-entity-parameter-name' | translate }}">
  185 + </mat-form-field>
  186 + <div fxFlex fxLayout="column">
  187 + <label class="tb-small">{{ 'alias.default-state-entity' | translate }}</label>
  188 + <tb-entity-select fxFlex
  189 + useAliasEntityTypes="true"
  190 + formControlName="defaultStateEntity">
  191 + </tb-entity-select>
  192 + </div>
  193 + </div>
  194 + <div fxFlex fxLayoutGap="8px" fxLayout="row">
  195 + <mat-form-field class="mat-block" style="min-width: 100px;">
  196 + <mat-label translate>relation.direction</mat-label>
  197 + <mat-select required matInput formControlName="direction">
  198 + <mat-option *ngFor="let type of directionTypes" [value]="type">
  199 + {{ directionTypeTranslations.get(type) | translate }}
  200 + </mat-option>
  201 + </mat-select>
  202 + </mat-form-field>
  203 + <mat-form-field fxFlex floatLabel="always" class="mat-block">
  204 + <mat-label translate>alias.max-relation-level</mat-label>
  205 + <input matInput
  206 + type="number"
  207 + min="1"
  208 + step="1"
  209 + placeholder="{{ 'alias.unlimited-level' | translate }}"
  210 + formControlName="maxLevel">
  211 + </mat-form-field>
  212 + </div>
  213 + <div class="mat-caption" style="color: rgba(0,0,0,0.57);" translate>relation.relation-type</div>
  214 + <tb-relation-type-autocomplete
  215 + fxFlex
  216 + formControlName="relationType">
  217 + </tb-relation-type-autocomplete>
  218 + </ng-template>
  219 + <ng-template [ngSwitchCase]="aliasFilterType.assetSearchQuery">
  220 + <div class="mat-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>asset.asset-types</div>
  221 + <tb-entity-subtype-list
  222 + required
  223 + [entityType]="entityType.ASSET"
  224 + formControlName="assetTypes">
  225 + </tb-entity-subtype-list>
  226 + </ng-template>
  227 + <ng-template [ngSwitchCase]="aliasFilterType.deviceSearchQuery">
  228 + <div class="mat-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>device.device-types</div>
  229 + <tb-entity-subtype-list
  230 + required
  231 + [entityType]="entityType.DEVICE"
  232 + formControlName="deviceTypes">
  233 + </tb-entity-subtype-list>
  234 + </ng-template>
  235 + <ng-template [ngSwitchCase]="aliasFilterType.entityViewSearchQuery">
  236 + <div class="mat-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>entity-view.entity-view-types</div>
  237 + <tb-entity-subtype-list
  238 + required
  239 + [entityType]="entityType.ENTITY_VIEW"
  240 + formControlName="entityViewTypes">
  241 + </tb-entity-subtype-list>
  242 + </ng-template>
  243 + </section>
  244 +</div>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .tb-entity-filter {
  18 + #relationsQueryFilter {
  19 + padding-top: 20px;
  20 +
  21 + tb-entity-select {
  22 + min-height: 92px;
  23 + }
  24 + }
  25 +
  26 + .tb-root-state-entity-switch {
  27 + padding-left: 10px;
  28 +
  29 + .root-state-entity-switch {
  30 + margin: 0;
  31 + }
  32 +
  33 + .root-state-entity-label {
  34 + margin: 5px 0 5px 10px;
  35 + }
  36 + }
  37 + }
  38 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
  18 +import {
  19 + ControlValueAccessor,
  20 + FormBuilder, FormControl,
  21 + FormGroup,
  22 + NG_VALUE_ACCESSOR,
  23 + ValidatorFn,
  24 + Validators
  25 +} from '@angular/forms';
  26 +import { AliasFilterType, aliasFilterTypeTranslationMap, EntityAliasFilter } from '@shared/models/alias.models';
  27 +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
  28 +import { TranslateService } from '@ngx-translate/core';
  29 +import { EntityService } from '@core/http/entity.service';
  30 +import { EntitySearchDirection, entitySearchDirectionTranslations } from '@shared/models/relation.models';
  31 +
  32 +@Component({
  33 + selector: 'tb-entity-filter',
  34 + templateUrl: './entity-filter.component.html',
  35 + styleUrls: ['./entity-filter.component.scss'],
  36 + providers: [
  37 + {
  38 + provide: NG_VALUE_ACCESSOR,
  39 + useExisting: forwardRef(() => EntityFilterComponent),
  40 + multi: true
  41 + }
  42 + ]
  43 +})
  44 +export class EntityFilterComponent implements ControlValueAccessor, OnInit {
  45 +
  46 + @Input() disabled: boolean;
  47 +
  48 + @Input() allowedEntityTypes: Array<EntityType | AliasEntityType>;
  49 +
  50 + @Input() resolveMultiple: boolean;
  51 +
  52 + @Output() resolveMultipleChanged: EventEmitter<boolean> = new EventEmitter<boolean>();
  53 +
  54 + entityFilterFormGroup: FormGroup;
  55 + filterFormGroup: FormGroup;
  56 +
  57 + aliasFilterTypes: Array<AliasFilterType>;
  58 +
  59 + aliasFilterType = AliasFilterType;
  60 + aliasFilterTypeTranslations = aliasFilterTypeTranslationMap;
  61 + entityType = EntityType;
  62 +
  63 + directionTypes = Object.keys(EntitySearchDirection);
  64 + directionTypeTranslations = entitySearchDirectionTranslations;
  65 +
  66 + private propagateChange = null;
  67 +
  68 + constructor(private translate: TranslateService,
  69 + private entityService: EntityService,
  70 + private fb: FormBuilder) {
  71 + }
  72 +
  73 + ngOnInit(): void {
  74 +
  75 + this.aliasFilterTypes = this.entityService.getAliasFilterTypesByEntityTypes(this.allowedEntityTypes);
  76 +
  77 + this.entityFilterFormGroup = this.fb.group({
  78 + type: [null, [Validators.required]]
  79 + });
  80 + this.entityFilterFormGroup.get('type').valueChanges.subscribe((type: AliasFilterType) => {
  81 + this.filterTypeChanged(type);
  82 + });
  83 + this.entityFilterFormGroup.valueChanges.subscribe(() => {
  84 + this.updateModel();
  85 + });
  86 + this.filterFormGroup = this.fb.group({});
  87 + }
  88 +
  89 + registerOnChange(fn: any): void {
  90 + this.propagateChange = fn;
  91 + }
  92 +
  93 + registerOnTouched(fn: any): void {
  94 + }
  95 +
  96 + setDisabledState?(isDisabled: boolean): void {
  97 + this.disabled = isDisabled;
  98 + }
  99 +
  100 + writeValue(filter: EntityAliasFilter): void {
  101 + if (!filter) {
  102 + filter = {
  103 + type: null,
  104 + resolveMultiple: this.resolveMultiple
  105 + };
  106 + }
  107 + this.entityFilterFormGroup.get('type').patchValue(filter.type, {emitEvent: false});
  108 + if (filter && filter.type) {
  109 + this.updateFilterFormGroup(filter.type, filter);
  110 + }
  111 + }
  112 +
  113 + private updateFilterFormGroup(type: AliasFilterType, filter?: EntityAliasFilter) {
  114 + switch (type) {
  115 + case AliasFilterType.singleEntity:
  116 + this.filterFormGroup = this.fb.group({
  117 + singleEntity: [filter ? filter.singleEntity : null, [Validators.required]]
  118 + });
  119 + break;
  120 + case AliasFilterType.entityList:
  121 + this.filterFormGroup = this.fb.group({
  122 + entityType: [filter ? filter.entityType : null, [Validators.required]],
  123 + entityList: [filter ? filter.entityList : [], [Validators.required]],
  124 + });
  125 + break;
  126 + case AliasFilterType.entityName:
  127 + this.filterFormGroup = this.fb.group({
  128 + entityType: [filter ? filter.entityType : null, [Validators.required]],
  129 + entityNameFilter: [filter ? filter.entityNameFilter : '', [Validators.required]],
  130 + });
  131 + break;
  132 + case AliasFilterType.stateEntity:
  133 + this.filterFormGroup = this.fb.group({
  134 + stateEntityParamName: [filter ? filter.stateEntityParamName : null, []],
  135 + defaultStateEntity: [filter ? filter.defaultStateEntity : null, []],
  136 + });
  137 + break;
  138 + case AliasFilterType.assetType:
  139 + this.filterFormGroup = this.fb.group({
  140 + assetType: [filter ? filter.assetType : null, [Validators.required]],
  141 + assetNameFilter: [filter ? filter.assetNameFilter : '', []],
  142 + });
  143 + break;
  144 + case AliasFilterType.deviceType:
  145 + this.filterFormGroup = this.fb.group({
  146 + deviceType: [filter ? filter.deviceType : null, [Validators.required]],
  147 + deviceNameFilter: [filter ? filter.deviceNameFilter : '', []],
  148 + });
  149 + break;
  150 + case AliasFilterType.entityViewType:
  151 + this.filterFormGroup = this.fb.group({
  152 + entityViewType: [filter ? filter.entityViewType : null, [Validators.required]],
  153 + entityViewNameFilter: [filter ? filter.entityViewNameFilter : '', []],
  154 + });
  155 + break;
  156 + case AliasFilterType.relationsQuery:
  157 + case AliasFilterType.assetSearchQuery:
  158 + case AliasFilterType.deviceSearchQuery:
  159 + case AliasFilterType.entityViewSearchQuery:
  160 + this.filterFormGroup = this.fb.group({
  161 + rootStateEntity: [filter ? filter.rootStateEntity : false, []],
  162 + stateEntityParamName: [filter ? filter.stateEntityParamName : null, []],
  163 + defaultStateEntity: [filter ? filter.defaultStateEntity : null, []],
  164 + rootEntity: [filter ? filter.rootEntity : null, (filter && filter.rootStateEntity) ? [] : [Validators.required]],
  165 + direction: [filter ? filter.direction : EntitySearchDirection.FROM, [Validators.required]],
  166 + maxLevel: [filter ? filter.maxLevel : 1, []],
  167 + });
  168 + this.filterFormGroup.get('rootStateEntity').valueChanges.subscribe((rootStateEntity: boolean) => {
  169 + this.filterFormGroup.get('rootEntity').setValidators(rootStateEntity ? [] : [Validators.required]);
  170 + this.filterFormGroup.get('rootEntity').updateValueAndValidity();
  171 + });
  172 + if (type === AliasFilterType.relationsQuery) {
  173 + this.filterFormGroup.addControl('filters',
  174 + this.fb.control(filter ? filter.filters : [], []));
  175 + } else {
  176 + this.filterFormGroup.addControl('relationType',
  177 + this.fb.control(filter ? filter.relationType : null, []));
  178 + if (type === AliasFilterType.assetSearchQuery) {
  179 + this.filterFormGroup.addControl('assetTypes',
  180 + this.fb.control(filter ? filter.assetTypes : [], [Validators.required]));
  181 + } else if (type === AliasFilterType.deviceSearchQuery) {
  182 + this.filterFormGroup.addControl('deviceTypes',
  183 + this.fb.control(filter ? filter.deviceTypes : [], [Validators.required]));
  184 + } else if (type === AliasFilterType.entityViewSearchQuery) {
  185 + this.filterFormGroup.addControl('entityViewTypes',
  186 + this.fb.control(filter ? filter.entityViewTypes : [], [Validators.required]));
  187 + }
  188 + }
  189 + break;
  190 + }
  191 + this.filterFormGroup.valueChanges.subscribe(() => {
  192 + this.updateModel();
  193 + });
  194 + }
  195 +
  196 + private filterTypeChanged(type: AliasFilterType) {
  197 + let resolveMultiple = true;
  198 + if (type === AliasFilterType.singleEntity || type === AliasFilterType.stateEntity) {
  199 + resolveMultiple = false;
  200 + }
  201 + if (this.resolveMultiple !== resolveMultiple) {
  202 + this.resolveMultipleChanged.emit(resolveMultiple);
  203 + }
  204 + this.updateFilterFormGroup(type);
  205 + }
  206 +
  207 + private updateModel() {
  208 + let filter = null;
  209 + if (this.entityFilterFormGroup.valid && this.filterFormGroup.valid) {
  210 + filter = {
  211 + type: this.entityFilterFormGroup.get('type').value,
  212 + resolveMultiple: this.resolveMultiple
  213 + };
  214 + filter = {...filter, ...this.filterFormGroup.value};
  215 + }
  216 + this.propagateChange(filter);
  217 + }
  218 +}
@@ -41,6 +41,11 @@ import { LegendComponent } from '@home/components/widget/legend.component'; @@ -41,6 +41,11 @@ import { LegendComponent } from '@home/components/widget/legend.component';
41 import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component'; 41 import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component';
42 import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component'; 42 import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component';
43 import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; 43 import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
  44 +import { EntityAliasesDialogComponent } from '@home/components/alias/entity-aliases-dialog.component';
  45 +import { EntityFilterViewComponent } from './entity/entity-filter-view.component';
  46 +import { EntityAliasDialogComponent } from './alias/entity-alias-dialog.component';
  47 +import { EntityFilterComponent } from './entity/entity-filter.component';
  48 +import { RelationFiltersComponent } from './relation/relation-filters.component';
44 49
45 @NgModule({ 50 @NgModule({
46 entryComponents: [ 51 entryComponents: [
@@ -52,7 +57,9 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com @@ -52,7 +57,9 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com
52 AlarmDetailsDialogComponent, 57 AlarmDetailsDialogComponent,
53 AddAttributeDialogComponent, 58 AddAttributeDialogComponent,
54 EditAttributeValuePanelComponent, 59 EditAttributeValuePanelComponent,
55 - AliasesEntitySelectPanelComponent 60 + AliasesEntitySelectPanelComponent,
  61 + EntityAliasesDialogComponent,
  62 + EntityAliasDialogComponent
56 ], 63 ],
57 declarations: 64 declarations:
58 [ 65 [
@@ -67,6 +74,7 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com @@ -67,6 +74,7 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com
67 EventTableComponent, 74 EventTableComponent,
68 RelationTableComponent, 75 RelationTableComponent,
69 RelationDialogComponent, 76 RelationDialogComponent,
  77 + RelationFiltersComponent,
70 AlarmTableHeaderComponent, 78 AlarmTableHeaderComponent,
71 AlarmTableComponent, 79 AlarmTableComponent,
72 AlarmDetailsDialogComponent, 80 AlarmDetailsDialogComponent,
@@ -75,10 +83,14 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com @@ -75,10 +83,14 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com
75 EditAttributeValuePanelComponent, 83 EditAttributeValuePanelComponent,
76 AliasesEntitySelectPanelComponent, 84 AliasesEntitySelectPanelComponent,
77 AliasesEntitySelectComponent, 85 AliasesEntitySelectComponent,
  86 + EntityAliasesDialogComponent,
  87 + EntityAliasDialogComponent,
78 DashboardComponent, 88 DashboardComponent,
79 WidgetComponent, 89 WidgetComponent,
80 LegendComponent, 90 LegendComponent,
81 - WidgetConfigComponent 91 + WidgetConfigComponent,
  92 + EntityFilterViewComponent,
  93 + EntityFilterComponent
82 ], 94 ],
83 imports: [ 95 imports: [
84 CommonModule, 96 CommonModule,
@@ -93,14 +105,19 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com @@ -93,14 +105,19 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com
93 AuditLogTableComponent, 105 AuditLogTableComponent,
94 EventTableComponent, 106 EventTableComponent,
95 RelationTableComponent, 107 RelationTableComponent,
  108 + RelationFiltersComponent,
96 AlarmTableComponent, 109 AlarmTableComponent,
97 AlarmDetailsDialogComponent, 110 AlarmDetailsDialogComponent,
98 AttributeTableComponent, 111 AttributeTableComponent,
99 AliasesEntitySelectComponent, 112 AliasesEntitySelectComponent,
  113 + EntityAliasesDialogComponent,
  114 + EntityAliasDialogComponent,
100 DashboardComponent, 115 DashboardComponent,
101 WidgetComponent, 116 WidgetComponent,
102 LegendComponent, 117 LegendComponent,
103 - WidgetConfigComponent 118 + WidgetConfigComponent,
  119 + EntityFilterViewComponent,
  120 + EntityFilterComponent
104 ], 121 ],
105 providers: [ 122 providers: [
106 WidgetComponentService 123 WidgetComponentService
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-relation-filters" [formGroup]="relationFiltersFormGroup">
  19 + <div class="header" [fxShow]="relationFiltersFormGroup.get('relationFilters').length">
  20 + <div fxLayout="row" fxLayoutAlign="start center">
  21 + <span class="cell" style="width: 200px; min-width: 200px;" translate>relation.type</span>
  22 + <span class="cell" fxFlex translate>entity.entity-types</span>
  23 + <span class="cell" style="width: 40px; min-width: 40px;">&nbsp;</span>
  24 + </div>
  25 + </div>
  26 + <div class="body" [fxShow]="relationFiltersFormGroup.get('relationFilters').length">
  27 + <div class="row" fxFlex fxLayout="row"
  28 + fxLayoutAlign="start center" formArrayName="relationFilters"
  29 + *ngFor="let relationFilterControl of relationFiltersFormGroup.get('relationFilters').controls; let $index = index">
  30 + <div class="mat-elevation-z1" fxFlex fxLayout="row" fxLayoutAlign="start center">
  31 + <tb-relation-type-autocomplete
  32 + class="cell" style="width: 200px; min-width: 200px;"
  33 + [formControl]="relationFilterControl.get('relationType')">
  34 + </tb-relation-type-autocomplete>
  35 + <tb-entity-type-list class="cell" fxFlex
  36 + [allowedEntityTypes]="allowedEntityTypes"
  37 + [formControl]="relationFilterControl.get('entityTypes')">
  38 + </tb-entity-type-list>
  39 + <button mat-button mat-icon-button color="primary"
  40 + type="button"
  41 + style="width: 40px; min-width: 40px;"
  42 + (click)="removeFilter($index)"
  43 + [disabled]="isLoading$ | async"
  44 + matTooltip="{{ 'relation.remove-relation-filter' | translate }}"
  45 + matTooltipPosition="above">
  46 + <mat-icon>close</mat-icon>
  47 + </button>
  48 + </div>
  49 + </div>
  50 + </div>
  51 + <div class="any-filter" [fxShow]="!relationFiltersFormGroup.get('relationFilters').length">
  52 + <span fxLayoutAlign="center center"
  53 + class="tb-prompt" translate>relation.any-relation</span>
  54 + </div>
  55 + <div>
  56 + <button mat-button mat-raised-button color="primary"
  57 + [disabled]="isLoading$ | async"
  58 + (click)="addFilter()"
  59 + type="button"
  60 + matTooltip="{{ 'relation.add-relation-filter' | translate }}"
  61 + matTooltipPosition="above">
  62 + <mat-icon>add</mat-icon>
  63 + {{ 'action.add' | translate }}
  64 + </button>
  65 + </div>
  66 +</div>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .tb-relation-filters {
  18 + .header {
  19 + padding-right: 5px;
  20 + padding-bottom: 5px;
  21 + padding-left: 5px;
  22 +
  23 + .cell {
  24 + padding-right: 5px;
  25 + padding-left: 5px;
  26 + font-size: 12px;
  27 + font-weight: 700;
  28 + color: rgba(0, 0, 0, .54);
  29 + white-space: nowrap;
  30 + }
  31 + }
  32 +
  33 + .body {
  34 + max-height: 300px;
  35 + padding-right: 5px;
  36 + padding-bottom: 20px;
  37 + padding-left: 5px;
  38 + overflow: auto;
  39 +
  40 + .row {
  41 + padding-top: 5px;
  42 + }
  43 +
  44 + .cell {
  45 + padding-right: 5px;
  46 + padding-left: 5px;
  47 + }
  48 + }
  49 + }
  50 +}
  51 +
  52 +:host ::ng-deep {
  53 + .tb-relation-filters {
  54 + .cell {
  55 + .mat-form-field {
  56 + .mat-form-field-infix {
  57 + border-top: none;
  58 + }
  59 + &.mat-form-field-appearance-standard {
  60 + .mat-form-field-flex {
  61 + padding-top: 0;
  62 + }
  63 + }
  64 + }
  65 + }
  66 + }
  67 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
  18 +import {
  19 + AbstractControl,
  20 + ControlValueAccessor, FormArray,
  21 + FormBuilder, FormControl,
  22 + FormGroup,
  23 + NG_VALUE_ACCESSOR,
  24 + ValidatorFn,
  25 + Validators
  26 +} from '@angular/forms';
  27 +import { AliasFilterType, aliasFilterTypeTranslationMap, EntityAliasFilter } from '@shared/models/alias.models';
  28 +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
  29 +import { TranslateService } from '@ngx-translate/core';
  30 +import { EntityService } from '@core/http/entity.service';
  31 +import { EntitySearchDirection, entitySearchDirectionTranslations, EntityTypeFilter } from '@shared/models/relation.models';
  32 +import { PageComponent } from '@shared/components/page.component';
  33 +import { Store } from '@ngrx/store';
  34 +import { AppState } from '@core/core.state';
  35 +import { Subscription } from 'rxjs';
  36 +
  37 +@Component({
  38 + selector: 'tb-relation-filters',
  39 + templateUrl: './relation-filters.component.html',
  40 + styleUrls: ['./relation-filters.component.scss'],
  41 + providers: [
  42 + {
  43 + provide: NG_VALUE_ACCESSOR,
  44 + useExisting: forwardRef(() => RelationFiltersComponent),
  45 + multi: true
  46 + }
  47 + ]
  48 +})
  49 +export class RelationFiltersComponent extends PageComponent implements ControlValueAccessor, OnInit {
  50 +
  51 + @Input() disabled: boolean;
  52 +
  53 + @Input() allowedEntityTypes: Array<EntityType | AliasEntityType>;
  54 +
  55 + relationFiltersFormGroup: FormGroup;
  56 +
  57 + private propagateChange = null;
  58 +
  59 + private valueChangeSubscription: Subscription = null;
  60 +
  61 + constructor(protected store: Store<AppState>,
  62 + private fb: FormBuilder) {
  63 + super(store);
  64 + }
  65 +
  66 + ngOnInit(): void {
  67 + this.relationFiltersFormGroup = this.fb.group({});
  68 + this.relationFiltersFormGroup.addControl('relationFilters',
  69 + this.fb.array([]));
  70 + }
  71 +
  72 + registerOnChange(fn: any): void {
  73 + this.propagateChange = fn;
  74 + }
  75 +
  76 + registerOnTouched(fn: any): void {
  77 + }
  78 +
  79 + setDisabledState?(isDisabled: boolean): void {
  80 + this.disabled = isDisabled;
  81 + }
  82 +
  83 + writeValue(filters: Array<EntityTypeFilter>): void {
  84 + if (this.valueChangeSubscription) {
  85 + this.valueChangeSubscription.unsubscribe();
  86 + }
  87 + const relationFiltersControls: Array<AbstractControl> = [];
  88 + if (filters && filters.length) {
  89 + filters.forEach((filter) => {
  90 + relationFiltersControls.push(this.createRelationFilterFormGroup(filter));
  91 + });
  92 + }
  93 + this.relationFiltersFormGroup.setControl('relationFilters', this.fb.array(relationFiltersControls));
  94 + this.valueChangeSubscription = this.relationFiltersFormGroup.valueChanges.subscribe(() => {
  95 + this.updateModel();
  96 + });
  97 + }
  98 +
  99 + public removeFilter(index: number) {
  100 + (this.relationFiltersFormGroup.get('relationFilters') as FormArray).removeAt(index);
  101 + }
  102 +
  103 + public addFilter() {
  104 + const relationFiltersFormArray = this.relationFiltersFormGroup.get('relationFilters') as FormArray;
  105 + const filter: EntityTypeFilter = {
  106 + relationType: null,
  107 + entityTypes: []
  108 + };
  109 + relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter));
  110 + }
  111 +
  112 + private createRelationFilterFormGroup(filter: EntityTypeFilter): AbstractControl {
  113 + return this.fb.group({
  114 + relationType: [filter ? filter.relationType : null],
  115 + entityTypes: [filter ? filter.entityTypes : []]
  116 + });
  117 + }
  118 +
  119 + private updateModel() {
  120 + const filters: Array<EntityTypeFilter> = this.relationFiltersFormGroup.get('relationFilters').value;
  121 + this.propagateChange(filters);
  122 + }
  123 +}
@@ -16,26 +16,121 @@ @@ -16,26 +16,121 @@
16 16
17 --> 17 -->
18 <mat-tab-group [ngClass]="{'tb-headless': (widgetType === widgetTypes.static && !displayAdvanced())}" 18 <mat-tab-group [ngClass]="{'tb-headless': (widgetType === widgetTypes.static && !displayAdvanced())}"
19 - fxFlex class="tb-widget-config tb-absolute-fill" [selectedIndex]="selectedTab"> 19 + fxFlex class="tb-widget-config tb-absolute-fill" [(selectedIndex)]="selectedTab">
20 <mat-tab label="{{ 'widget-config.data' | translate }}" [fxShow]="widgetType !== widgetTypes.static"> 20 <mat-tab label="{{ 'widget-config.data' | translate }}" [fxShow]="widgetType !== widgetTypes.static">
21 - <div class="mat-content mat-padding" fxLayout="column">  
22 - <div [fxShow]="widgetType === widgetTypes.timeseries || widgetType === widgetTypes.alarm"  
23 - fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center">  
24 - <div fxLayout="column" fxFlex>  
25 - <mat-checkbox fxFlex [(ngModel)]="useDashboardTimewindow" (ngModelChange)="updateModel()"> 21 + <div [formGroup]="dataSettings" class="mat-content mat-padding" fxLayout="column" fxLayoutGap="8px">
  22 + <div *ngIf="widgetType === widgetTypes.timeseries || widgetType === widgetTypes.alarm"
  23 + fxLayout="column" fxLayoutGap="8px" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center">
  24 + <div fxLayout="column" fxLayoutGap="8px" fxFlex>
  25 + <mat-checkbox fxFlex formControlName="useDashboardTimewindow">
26 {{ 'widget-config.use-dashboard-timewindow' | translate }} 26 {{ 'widget-config.use-dashboard-timewindow' | translate }}
27 </mat-checkbox> 27 </mat-checkbox>
28 - <mat-checkbox [disabled]="useDashboardTimewindow" fxFlex [(ngModel)]="displayTimewindow" (ngModelChange)="updateModel()"> 28 + <mat-checkbox fxFlex
  29 + formControlName="displayTimewindow">
29 {{ 'widget-config.display-timewindow' | translate }} 30 {{ 'widget-config.display-timewindow' | translate }}
30 </mat-checkbox> 31 </mat-checkbox>
31 </div> 32 </div>
32 <section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;"> 33 <section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;">
33 <span [ngClass]="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span> 34 <span [ngClass]="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span>
34 - <tb-timewindow [disabled]="useDashboardTimewindow" asButton="true" 35 + <tb-timewindow asButton="true"
35 aggregation="{{ widgetType === widgetTypes.timeseries }}" 36 aggregation="{{ widgetType === widgetTypes.timeseries }}"
36 - fxFlex [(ngModel)]="timewindow" (ngModelChange)="updateModel()"></tb-timewindow> 37 + fxFlex formControlName="timewindow"></tb-timewindow>
37 </section> 38 </section>
38 </div> 39 </div>
  40 + <div *ngIf="widgetType === widgetTypes.alarm" fxLayout="column" fxLayoutGap="8px" fxLayoutAlign="center"
  41 + fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center">
  42 + <mat-form-field fxFlex class="mat-block">
  43 + <mat-label translate>alarm.alarm-status</mat-label>
  44 + <mat-select matInput formControlName="alarmSearchStatus">
  45 + <mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus">
  46 + {{ ('alarm.search-status.' + searchStatus) | translate }}
  47 + </mat-option>
  48 + </mat-select>
  49 + </mat-form-field>
  50 + <mat-form-field fxFlex class="mat-block">
  51 + <mat-label translate>alarm.polling-interval</mat-label>
  52 + <input matInput required
  53 + formControlName="alarmsPollingInterval"
  54 + type="number"
  55 + step="1"/>
  56 + <mat-error *ngIf="dataSettings.get('alarmsPollingInterval').hasError('required')">
  57 + {{ 'alarm.polling-interval-required' | translate }}
  58 + </mat-error>
  59 + <mat-error *ngIf="dataSettings.get('alarmsPollingInterval').hasError('min')">
  60 + {{ 'alarm.min-polling-interval-message' | translate }}
  61 + </mat-error>
  62 + </mat-form-field>
  63 + </div>
  64 + <mat-expansion-panel class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc &&
  65 + widgetType !== widgetTypes.alarm &&
  66 + widgetType !== widgetTypes.static &&
  67 + isDataEnabled" [expanded]="true">
  68 + <mat-expansion-panel-header>
  69 + <mat-panel-title fxLayout="column">
  70 + <div class="tb-panel-title" translate>widget-config.datasources</div>
  71 + <div *ngIf="typeParameters && typeParameters.maxDatasources > -1"
  72 + class="tb-hint">{{ 'widget-config.maximum-datasources' | translate:{count: typeParameters.maxDatasources} }}</div>
  73 + </mat-panel-title>
  74 + </mat-expansion-panel-header>
  75 + <div *ngIf="dataSettings.get('datasources').length === 0; else datasourcesTemplate">
  76 + <span translate fxLayoutAlign="center center"
  77 + class="tb-prompt">datasource.add-datasource-prompt</span>
  78 + </div>
  79 + <ng-template #datasourcesTemplate>
  80 + <div fxFlex fxLayout="row" fxLayoutAlign="start center">
  81 + <span fxFlex="5"></span>
  82 + <div fxFlex fxLayout="row" fxLayoutAlign="start center"
  83 + style="padding: 0 0 0 10px; margin: 5px;">
  84 + <span translate style="min-width: 110px;">widget-config.datasource-type</span>
  85 + <span fxHide fxShow.gt-sm translate fxFlex
  86 + style="padding-left: 10px;">widget-config.datasource-parameters</span>
  87 + <span style="min-width: 40px;"></span>
  88 + </div>
  89 + </div>
  90 + <div style="overflow: auto; padding-bottom: 15px;">
  91 + <div fxFlex fxLayout="row" fxLayoutAlign="start center"
  92 + formArrayName="datasources"
  93 + *ngFor="let datasourceControl of dataSettings.get('datasources').controls; let $index = index">
  94 + <span fxFlex="5">{{$index + 1}}.</span>
  95 + <div class="mat-elevation-z4" fxFlex fxLayout="row" fxLayoutAlign="start center"
  96 + style="padding: 0 0 0 10px; margin: 5px;">
  97 + <div fxFlex>TODO: datasource: {{ datasourceControl.value | json }}</div>
  98 + <button [disabled]="isLoading$ | async"
  99 + mat-button mat-icon-button color="primary"
  100 + style="min-width: 40px;"
  101 + (click)="removeDatasource($index)"
  102 + matTooltip="{{ 'widget-config.remove-datasource' | translate }}"
  103 + matTooltipPosition="above">
  104 + <mat-icon>close</mat-icon>
  105 + </button>
  106 + </div>
  107 + </div>
  108 + </div>
  109 + </ng-template>
  110 + <div fxFlex fxLayout="row" fxLayoutAlign="start center">
  111 + <button [disabled]="isLoading$ | async"
  112 + mat-button mat-raised-button color="primary"
  113 + [fxShow]="typeParameters &&
  114 + (typeParameters.maxDatasources == -1 || dataSettings.get('datasources').controls.length < typeParameters.maxDatasources)"
  115 + (click)="addDatasource()"
  116 + matTooltip="{{ 'widget-config.add-datasource' | translate }}"
  117 + matTooltipPosition="above">
  118 + <mat-icon>add</mat-icon>
  119 + <span translate>action.add</span>
  120 + </button>
  121 + </div>
  122 + </mat-expansion-panel>
  123 + <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.rpc &&
  124 + isDataEnabled" [expanded]="true">
  125 + <mat-expansion-panel-header>
  126 + <mat-panel-title>
  127 + {{ 'widget-config.target-device' | translate }}
  128 + </mat-panel-title>
  129 + </mat-expansion-panel-header>
  130 + <div style="padding: 0 5px;">
  131 +
  132 + </div>
  133 + </mat-expansion-panel>
39 </div> 134 </div>
40 </mat-tab> 135 </mat-tab>
41 <mat-tab label="{{ 'widget-config.settings' | translate }}"> 136 <mat-tab label="{{ 'widget-config.settings' | translate }}">
@@ -28,5 +28,8 @@ @@ -28,5 +28,8 @@
28 height: 100%; 28 height: 100%;
29 } 29 }
30 } 30 }
  31 + .mat-expansion-panel.tb-datasources.mat-expanded {
  32 + overflow: visible;
  33 + }
31 } 34 }
32 } 35 }
@@ -19,20 +19,35 @@ import { PageComponent } from '@shared/components/page.component'; @@ -19,20 +19,35 @@ import { PageComponent } from '@shared/components/page.component';
19 import { Store } from '@ngrx/store'; 19 import { Store } from '@ngrx/store';
20 import { AppState } from '@core/core.state'; 20 import { AppState } from '@core/core.state';
21 import { 21 import {
  22 + DataKey,
22 Datasource, 23 Datasource,
  24 + DatasourceType,
23 LegendConfig, 25 LegendConfig,
24 WidgetActionDescriptor, 26 WidgetActionDescriptor,
25 - WidgetActionSource, WidgetConfigSettings, 27 + WidgetActionSource,
  28 + WidgetConfigSettings,
26 widgetType, 29 widgetType,
27 WidgetTypeParameters 30 WidgetTypeParameters
28 } from '@shared/models/widget.models'; 31 } from '@shared/models/widget.models';
29 -import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; 32 +import {
  33 + ControlValueAccessor,
  34 + FormArray,
  35 + FormBuilder,
  36 + FormControl,
  37 + FormGroup,
  38 + NG_VALIDATORS,
  39 + NG_VALUE_ACCESSOR,
  40 + Validator,
  41 + Validators
  42 +} from '@angular/forms';
30 import { WidgetConfigComponentData } from '@home/models/widget-component.models'; 43 import { WidgetConfigComponentData } from '@home/models/widget-component.models';
31 -import { deepClone, isDefined } from '@app/core/utils';  
32 -import { Timewindow } from '@shared/models/time/time.models';  
33 -import { AlarmSearchStatus } from '@shared/models/alarm.models'; 44 +import { deepClone, isDefined, isObject } from '@app/core/utils';
  45 +import { alarmFields, AlarmSearchStatus } from '@shared/models/alarm.models';
34 import { IAliasController } from '@core/api/widget-api.models'; 46 import { IAliasController } from '@core/api/widget-api.models';
35 import { EntityAlias } from '@shared/models/alias.models'; 47 import { EntityAlias } from '@shared/models/alias.models';
  48 +import { UtilsService } from '@core/services/utils.service';
  49 +import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
  50 +import { TranslateService } from '@ngx-translate/core';
36 51
37 @Component({ 52 @Component({
38 selector: 'tb-widget-config', 53 selector: 'tb-widget-config',
@@ -55,6 +70,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -55,6 +70,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
55 70
56 widgetTypes = widgetType; 71 widgetTypes = widgetType;
57 72
  73 + alarmSearchStatuses = Object.keys(AlarmSearchStatus);
  74 +
58 @Input() 75 @Input()
59 forceExpandDatasources: boolean; 76 forceExpandDatasources: boolean;
60 77
@@ -62,9 +79,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -62,9 +79,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
62 isDataEnabled: boolean; 79 isDataEnabled: boolean;
63 80
64 @Input() 81 @Input()
65 - widgetType: widgetType;  
66 -  
67 - @Input()  
68 typeParameters: WidgetTypeParameters; 82 typeParameters: WidgetTypeParameters;
69 83
70 @Input() 84 @Input()
@@ -84,6 +98,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -84,6 +98,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
84 98
85 @Input() disabled: boolean; 99 @Input() disabled: boolean;
86 100
  101 + widgetType: widgetType;
  102 +
87 selectedTab: number; 103 selectedTab: number;
88 title: string; 104 title: string;
89 showTitleIcon: boolean; 105 showTitleIcon: boolean;
@@ -101,17 +117,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -101,17 +117,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
101 titleStyle: string; 117 titleStyle: string;
102 units: string; 118 units: string;
103 decimals: number; 119 decimals: number;
104 - useDashboardTimewindow: boolean;  
105 - displayTimewindow: boolean;  
106 - timewindow: Timewindow;  
107 showLegend: boolean; 120 showLegend: boolean;
108 legendConfig: LegendConfig; 121 legendConfig: LegendConfig;
109 actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; 122 actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
110 - datasources: Array<Datasource>;  
111 targetDeviceAlias: EntityAlias; 123 targetDeviceAlias: EntityAlias;
112 alarmSource: Datasource; 124 alarmSource: Datasource;
113 - alarmSearchStatus: AlarmSearchStatus;  
114 - alarmsPollingInterval: number;  
115 settings: WidgetConfigSettings; 125 settings: WidgetConfigSettings;
116 mobileOrder: number; 126 mobileOrder: number;
117 mobileHeight: number; 127 mobileHeight: number;
@@ -138,11 +148,51 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -138,11 +148,51 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
138 148
139 private propagateChange = null; 149 private propagateChange = null;
140 150
141 - constructor(protected store: Store<AppState>) { 151 + public dataSettings: FormGroup;
  152 +
  153 + constructor(protected store: Store<AppState>,
  154 + private utils: UtilsService,
  155 + private translate: TranslateService,
  156 + private fb: FormBuilder) {
142 super(store); 157 super(store);
143 } 158 }
144 159
145 ngOnInit(): void { 160 ngOnInit(): void {
  161 +
  162 + }
  163 +
  164 + private buildForms() {
  165 + this.dataSettings = this.fb.group({});
  166 + if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
  167 + this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null));
  168 + this.dataSettings.addControl('displayTimewindow', this.fb.control(null));
  169 + this.dataSettings.addControl('timewindow', this.fb.control(null));
  170 + this.dataSettings.get('useDashboardTimewindow').valueChanges.subscribe((value: boolean) => {
  171 + if (value) {
  172 + this.dataSettings.get('displayTimewindow').disable({emitEvent: false});
  173 + this.dataSettings.get('timewindow').disable({emitEvent: false});
  174 + } else {
  175 + this.dataSettings.get('displayTimewindow').enable({emitEvent: false});
  176 + this.dataSettings.get('timewindow').enable({emitEvent: false});
  177 + }
  178 + });
  179 + if (this.widgetType === widgetType.alarm) {
  180 + this.dataSettings.addControl('alarmSearchStatus', this.fb.control(null));
  181 + this.dataSettings.addControl('alarmsPollingInterval', this.fb.control(null,
  182 + [Validators.required, Validators.min(1)]));
  183 + }
  184 + }
  185 + if (this.isDataEnabled) {
  186 + if (this.widgetType !== widgetType.rpc &&
  187 + this.widgetType !== widgetType.alarm &&
  188 + this.widgetType !== widgetType.static) {
  189 + this.dataSettings.addControl('datasources',
  190 + this.fb.array([]));
  191 + }
  192 + }
  193 + this.dataSettings.valueChanges.subscribe(
  194 + () => this.updateModel()
  195 + );
146 } 196 }
147 197
148 registerOnChange(fn: any): void { 198 registerOnChange(fn: any): void {
@@ -159,6 +209,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -159,6 +209,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
159 writeValue(value: WidgetConfigComponentData): void { 209 writeValue(value: WidgetConfigComponentData): void {
160 this.modelValue = value; 210 this.modelValue = value;
161 if (this.modelValue) { 211 if (this.modelValue) {
  212 + if (this.widgetType !== this.modelValue.widgetType) {
  213 + this.widgetType = this.modelValue.widgetType;
  214 + this.buildForms();
  215 + }
162 const config = this.modelValue.config; 216 const config = this.modelValue.config;
163 const layout = this.modelValue.layout; 217 const layout = this.modelValue.layout;
164 if (config) { 218 if (config) {
@@ -184,23 +238,42 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -184,23 +238,42 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
184 }, null, 2); 238 }, null, 2);
185 this.units = config.units; 239 this.units = config.units;
186 this.decimals = config.decimals; 240 this.decimals = config.decimals;
187 - this.useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ?  
188 - config.useDashboardTimewindow : true;  
189 - this.displayTimewindow = isDefined(config.displayTimewindow) ?  
190 - config.displayTimewindow : true;  
191 - this.timewindow = config.timewindow;  
192 this.actions = config.actions; 241 this.actions = config.actions;
193 if (!this.actions) { 242 if (!this.actions) {
194 this.actions = {}; 243 this.actions = {};
195 } 244 }
  245 + if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
  246 + const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ?
  247 + config.useDashboardTimewindow : true;
  248 + this.dataSettings.patchValue(
  249 + { useDashboardTimewindow }, {emitEvent: false}
  250 + );
  251 + if (useDashboardTimewindow) {
  252 + this.dataSettings.get('displayTimewindow').disable({emitEvent: false});
  253 + this.dataSettings.get('timewindow').disable({emitEvent: false});
  254 + } else {
  255 + this.dataSettings.get('displayTimewindow').enable({emitEvent: false});
  256 + this.dataSettings.get('timewindow').enable({emitEvent: false});
  257 + }
  258 + this.dataSettings.patchValue(
  259 + { displayTimewindow: isDefined(config.displayTimewindow) ?
  260 + config.displayTimewindow : true }, {emitEvent: false}
  261 + );
  262 + this.dataSettings.patchValue(
  263 + { timewindow: config.timewindow }, {emitEvent: false}
  264 + );
  265 + }
196 if (this.isDataEnabled) { 266 if (this.isDataEnabled) {
197 if (this.widgetType !== widgetType.rpc && 267 if (this.widgetType !== widgetType.rpc &&
198 this.widgetType !== widgetType.alarm && 268 this.widgetType !== widgetType.alarm &&
199 this.widgetType !== widgetType.static) { 269 this.widgetType !== widgetType.static) {
  270 + const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray;
  271 + datasourcesFormArray.controls.length = 0;
200 if (config.datasources) { 272 if (config.datasources) {
201 - this.datasources = config.datasources;  
202 - } else {  
203 - this.datasources = []; 273 + config.datasources.forEach((datasource) => {
  274 + datasourcesFormArray.controls.push(this.fb.control(datasource));
  275 + });
  276 + datasourcesFormArray.setValue(config.datasources, {emitEvent: false});
204 } 277 }
205 } else if (this.widgetType === widgetType.rpc) { 278 } else if (this.widgetType === widgetType.rpc) {
206 if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) { 279 if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) {
@@ -215,10 +288,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -215,10 +288,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
215 this.targetDeviceAlias = null; 288 this.targetDeviceAlias = null;
216 } 289 }
217 } else if (this.widgetType === widgetType.alarm) { 290 } else if (this.widgetType === widgetType.alarm) {
218 - this.alarmSearchStatus = isDefined(config.alarmSearchStatus) ?  
219 - config.alarmSearchStatus : AlarmSearchStatus.ANY;  
220 - this.alarmsPollingInterval = isDefined(config.alarmsPollingInterval) ?  
221 - config.alarmsPollingInterval : 5; 291 + this.dataSettings.patchValue(
  292 + { alarmSearchStatus: isDefined(config.alarmSearchStatus) ?
  293 + config.alarmSearchStatus : AlarmSearchStatus.ANY }, {emitEvent: false}
  294 + );
  295 + this.dataSettings.patchValue(
  296 + { alarmsPollingInterval: isDefined(config.alarmsPollingInterval) ?
  297 + config.alarmsPollingInterval : 5}, {emitEvent: false}
  298 + );
222 if (config.alarmSource) { 299 if (config.alarmSource) {
223 this.alarmSource = config.alarmSource; 300 this.alarmSource = config.alarmSource;
224 } else { 301 } else {
@@ -255,13 +332,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -255,13 +332,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
255 } 332 }
256 } 333 }
257 334
258 - public updateModel() { 335 + private updateModel() {
259 if (this.modelValue) { 336 if (this.modelValue) {
260 if (this.modelValue.config) { 337 if (this.modelValue.config) {
261 - const config = this.modelValue.config;  
262 - config.useDashboardTimewindow = this.useDashboardTimewindow;  
263 - config.displayTimewindow = this.displayTimewindow;  
264 - config.timewindow = this.timewindow; 338 + Object.assign(this.modelValue.config, this.dataSettings.value);
265 } 339 }
266 this.propagateChange(this.modelValue); 340 this.propagateChange(this.modelValue);
267 } 341 }
@@ -271,12 +345,150 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -271,12 +345,150 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
271 return this.widgetSettingsSchema && this.widgetSettingsSchema.schema; 345 return this.widgetSettingsSchema && this.widgetSettingsSchema.schema;
272 } 346 }
273 347
  348 + public removeDatasource(index: number) {
  349 + (this.dataSettings.get('datasources') as FormArray).removeAt(index);
  350 + }
  351 +
  352 + public addDatasource() {
  353 + let newDatasource: Datasource;
  354 + if (this.functionsOnly) {
  355 + newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema));
  356 + newDatasource.dataKeys = [this.generateDataKey('Sin', DataKeyType.function)];
  357 + } else {
  358 + newDatasource = { type: DatasourceType.entity,
  359 + dataKeys: []
  360 + };
  361 + }
  362 + const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray;
  363 + datasourcesFormArray.push(this.fb.control(newDatasource));
  364 + }
  365 +
  366 + public generateDataKey(chip: any, type: DataKeyType): DataKey {
  367 + if (isObject(chip)) {
  368 + (chip as DataKey)._hash = Math.random();
  369 + return chip;
  370 + } else {
  371 + let label: string = chip;
  372 + if (type === DataKeyType.alarm) {
  373 + const alarmField = alarmFields[label];
  374 + if (alarmField) {
  375 + label = this.translate.instant(alarmField.name);
  376 + }
  377 + }
  378 + label = this.genNextLabel(label);
  379 + const result: DataKey = {
  380 + name: chip,
  381 + type,
  382 + label,
  383 + color: this.genNextColor(),
  384 + settings: {},
  385 + _hash: Math.random()
  386 + };
  387 + if (type === DataKeyType.function) {
  388 + result.name = 'f(x)';
  389 + result.funcBody = this.utils.getPredefinedFunctionBody(chip);
  390 + if (!result.funcBody) {
  391 + result.funcBody = 'return prevValue + 1;';
  392 + }
  393 + }
  394 + if (isDefined(this.dataKeySettingsSchema.schema)) {
  395 + result.settings = this.utils.generateObjectFromJsonSchema(this.dataKeySettingsSchema.schema);
  396 + }
  397 + return result;
  398 + }
  399 + }
  400 +
  401 + private genNextLabel(name: string): string {
  402 + let label = name;
  403 + let i = 1;
  404 + let matches = false;
  405 + const datasources = this.widgetType === widgetType.alarm ? [this.modelValue.config.alarmSource] : this.modelValue.config.datasources;
  406 + if (datasources) {
  407 + do {
  408 + matches = false;
  409 + datasources.forEach((datasource) => {
  410 + if (datasource && datasource.dataKeys) {
  411 + datasource.dataKeys.forEach((dataKey) => {
  412 + if (dataKey.label === label) {
  413 + i++;
  414 + label = name + ' ' + i;
  415 + matches = true;
  416 + }
  417 + });
  418 + }
  419 + });
  420 + } while (matches);
  421 + }
  422 + return label;
  423 + }
  424 +
  425 + private genNextColor(): string {
  426 + let i = 0;
  427 + const datasources = this.widgetType === widgetType.alarm ? [this.modelValue.config.alarmSource] : this.modelValue.config.datasources;
  428 + if (datasources) {
  429 + datasources.forEach((datasource) => {
  430 + if (datasource && datasource.dataKeys) {
  431 + i += datasource.dataKeys.length;
  432 + }
  433 + });
  434 + }
  435 + return this.utils.getMaterialColor(i);
  436 + }
  437 +
274 public validate(c: FormControl) { 438 public validate(c: FormControl) {
275 - return null; /*{  
276 - targetDeviceAliasIds: {  
277 - valid: false,  
278 - },  
279 - };*/ 439 + if (!this.dataSettings.valid) {
  440 + return {
  441 + dataSettings: {
  442 + valid: false
  443 + }
  444 + };
  445 + } else {
  446 + const config = this.modelValue.config;
  447 + if (this.widgetType === widgetType.rpc && this.isDataEnabled) {
  448 + if (!config.targetDeviceAliasIds || !config.targetDeviceAliasIds.length) {
  449 + return {
  450 + targetDeviceAliasIds: {
  451 + valid: false
  452 + }
  453 + };
  454 + }
  455 + } else if (this.widgetType === widgetType.alarm && this.isDataEnabled) {
  456 + if (!config.alarmSource) {
  457 + return {
  458 + alarmSource: {
  459 + valid: false
  460 + }
  461 + };
  462 + }
  463 + } else if (this.widgetType !== widgetType.static && this.isDataEnabled) {
  464 + if (!config.datasources || !config.datasources.length) {
  465 + return {
  466 + datasources: {
  467 + valid: false
  468 + }
  469 + };
  470 + }
  471 + }
  472 + try {
  473 + JSON.parse(this.widgetStyle);
  474 + } catch (e) {
  475 + return {
  476 + widgetStyle: {
  477 + valid: false
  478 + }
  479 + };
  480 + }
  481 + try {
  482 + JSON.parse(this.titleStyle);
  483 + } catch (e) {
  484 + return {
  485 + titleStyle: {
  486 + valid: false
  487 + }
  488 + };
  489 + }
  490 + }
  491 + return null;
280 } 492 }
281 493
282 } 494 }
@@ -121,6 +121,7 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri @@ -121,6 +121,7 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
121 export interface WidgetConfigComponentData { 121 export interface WidgetConfigComponentData {
122 config: WidgetConfig; 122 config: WidgetConfig;
123 layout: WidgetLayout; 123 layout: WidgetLayout;
  124 + widgetType: widgetType;
124 } 125 }
125 126
126 export const MissingWidgetType: WidgetInfo = { 127 export const MissingWidgetType: WidgetInfo = {
@@ -18,7 +18,8 @@ @@ -18,7 +18,8 @@
18 <div class="tb-dashboard-page mat-content" style="padding-top: 150px;" 18 <div class="tb-dashboard-page mat-content" style="padding-top: 150px;"
19 fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen"> 19 fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
20 <section class="tb-dashboard-toolbar" 20 <section class="tb-dashboard-toolbar"
21 - [ngClass]="{ 'tb-dashboard-toolbar-opened': toolbarOpened, 'tb-dashboard-toolbar-closed': !toolbarOpened }"> 21 + [ngClass]="{ 'tb-dashboard-toolbar-opened': toolbarOpened,
  22 + 'tb-dashboard-toolbar-closed': !toolbarOpened }">
22 <tb-dashboard-toolbar [fxShow]="!widgetEditMode" [forceFullscreen]="forceFullscreen" 23 <tb-dashboard-toolbar [fxShow]="!widgetEditMode" [forceFullscreen]="forceFullscreen"
23 [toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()"> 24 [toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()">
24 <div class="tb-dashboard-action-panels" fxLayout="column-reverse" fxLayout.gt-sm="row-reverse" 25 <div class="tb-dashboard-action-panels" fxLayout="column-reverse" fxLayout.gt-sm="row-reverse"
@@ -117,6 +118,7 @@ @@ -117,6 +118,7 @@
117 <section class="tb-dashboard-container tb-absolute-fill" 118 <section class="tb-dashboard-container tb-absolute-fill"
118 [ngClass]="{ 'is-fullscreen': forceFullscreen, 119 [ngClass]="{ 'is-fullscreen': forceFullscreen,
119 'tb-dashboard-toolbar-opened': toolbarOpened, 120 'tb-dashboard-toolbar-opened': toolbarOpened,
  121 + 'tb-dashboard-toolbar-animated': isToolbarOpenedAnimate,
120 'tb-dashboard-toolbar-closed': !toolbarOpened }"> 122 'tb-dashboard-toolbar-closed': !toolbarOpened }">
121 <section *ngIf="!widgetEditMode" class="tb-dashboard-title" fxLayout="row" fxLayoutAlign="center center" 123 <section *ngIf="!widgetEditMode" class="tb-dashboard-title" fxLayout="row" fxLayoutAlign="center center"
122 [ngStyle]="{'color': dashboard.configuration.settings.titleColor}"> 124 [ngStyle]="{'color': dashboard.configuration.settings.titleColor}">
@@ -93,15 +93,18 @@ div.tb-dashboard-page { @@ -93,15 +93,18 @@ div.tb-dashboard-page {
93 @media #{$mat-gt-sm} { 93 @media #{$mat-gt-sm} {
94 margin-top: $toolbar-height; 94 margin-top: $toolbar-height;
95 } 95 }
96 -  
97 - transition: margin-top .3s cubic-bezier(.55, 0, .55, .2); 96 + &.tb-dashboard-toolbar-animated {
  97 + transition: margin-top .3s cubic-bezier(.55, 0, .55, .2);
  98 + }
98 } 99 }
99 } 100 }
100 101
101 &.tb-dashboard-toolbar-closed { 102 &.tb-dashboard-toolbar-closed {
102 margin-top: 0; 103 margin-top: 0;
103 104
104 - transition: margin-top .3s cubic-bezier(.55, 0, .55, .2) .2s; 105 + &.tb-dashboard-toolbar-animated {
  106 + transition: margin-top .3s cubic-bezier(.55, 0, .55, .2) .2s;
  107 + }
105 } 108 }
106 109
107 .tb-dashboard-layouts { 110 .tb-dashboard-layouts {
@@ -60,6 +60,17 @@ import { @@ -60,6 +60,17 @@ import {
60 import { WidgetComponentService } from '../../components/widget/widget-component.service'; 60 import { WidgetComponentService } from '../../components/widget/widget-component.service';
61 import { FormBuilder, FormGroup, NgForm } from '@angular/forms'; 61 import { FormBuilder, FormGroup, NgForm } from '@angular/forms';
62 import { ItemBufferService } from '@core/services/item-buffer.service'; 62 import { ItemBufferService } from '@core/services/item-buffer.service';
  63 +import {
  64 + DeviceCredentialsDialogComponent,
  65 + DeviceCredentialsDialogData
  66 +} from '@home/pages/device/device-credentials-dialog.component';
  67 +import { DeviceCredentials } from '@shared/models/device.models';
  68 +import { MatDialog } from '@angular/material/dialog';
  69 +import {
  70 + EntityAliasesDialogComponent,
  71 + EntityAliasesDialogData
  72 +} from '@home/components/alias/entity-aliases-dialog.component';
  73 +import { EntityAliases } from '@app/shared/models/alias.models';
63 74
64 @Component({ 75 @Component({
65 selector: 'tb-dashboard-page', 76 selector: 'tb-dashboard-page',
@@ -89,6 +100,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -89,6 +100,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
89 isAddingWidget = false; 100 isAddingWidget = false;
90 101
91 isToolbarOpened = false; 102 isToolbarOpened = false;
  103 + isToolbarOpenedAnimate = false;
92 isRightLayoutOpened = false; 104 isRightLayoutOpened = false;
93 105
94 editingWidget: Widget = null; 106 editingWidget: Widget = null;
@@ -189,7 +201,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -189,7 +201,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
189 private widgetComponentService: WidgetComponentService, 201 private widgetComponentService: WidgetComponentService,
190 private dashboardService: DashboardService, 202 private dashboardService: DashboardService,
191 private itembuffer: ItemBufferService, 203 private itembuffer: ItemBufferService,
192 - private fb: FormBuilder) { 204 + private fb: FormBuilder,
  205 + private dialog: MatDialog) {
193 super(store); 206 super(store);
194 207
195 this.editingWidgetFormGroup = this.fb.group({ 208 this.editingWidgetFormGroup = this.fb.group({
@@ -259,6 +272,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -259,6 +272,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
259 this.isAddingWidget = false; 272 this.isAddingWidget = false;
260 273
261 this.isToolbarOpened = false; 274 this.isToolbarOpened = false;
  275 + this.isToolbarOpenedAnimate = false;
262 this.isRightLayoutOpened = false; 276 this.isRightLayoutOpened = false;
263 277
264 this.editingWidget = null; 278 this.editingWidget = null;
@@ -283,10 +297,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -283,10 +297,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
283 } 297 }
284 298
285 public openToolbar() { 299 public openToolbar() {
  300 + this.isToolbarOpenedAnimate = true;
286 this.isToolbarOpened = true; 301 this.isToolbarOpened = true;
287 } 302 }
288 303
289 public closeToolbar() { 304 public closeToolbar() {
  305 + this.isToolbarOpenedAnimate = true;
290 this.isToolbarOpened = false; 306 this.isToolbarOpened = false;
291 } 307 }
292 308
@@ -420,8 +436,21 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -420,8 +436,21 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
420 if ($event) { 436 if ($event) {
421 $event.stopPropagation(); 437 $event.stopPropagation();
422 } 438 }
423 - // TODO:  
424 - this.dialogService.todo(); 439 + this.dialog.open<EntityAliasesDialogComponent, EntityAliasesDialogData,
  440 + EntityAliases>(EntityAliasesDialogComponent, {
  441 + disableClose: true,
  442 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  443 + data: {
  444 + entityAliases: deepClone(this.dashboard.configuration.entityAliases),
  445 + widgets: this.dashboardUtils.getWidgetsArray(this.dashboard),
  446 + isSingleEntityAlias: false
  447 + }
  448 + }).afterClosed().subscribe((entityAliases) => {
  449 + if (entityAliases) {
  450 + this.dashboard.configuration.entityAliases = entityAliases;
  451 + this.entityAliasesUpdated();
  452 + }
  453 + });
425 } 454 }
426 455
427 public openDashboardSettings($event: Event) { 456 public openDashboardSettings($event: Event) {
@@ -18,7 +18,6 @@ @@ -18,7 +18,6 @@
18 <form [formGroup]="widgetFormGroup"> 18 <form [formGroup]="widgetFormGroup">
19 <fieldset [disabled]="isLoading$ | async"> 19 <fieldset [disabled]="isLoading$ | async">
20 <tb-widget-config 20 <tb-widget-config
21 - [widgetType]="widget.type"  
22 [typeParameters]="typeParameters" 21 [typeParameters]="typeParameters"
23 [actionSources]="actionSources" 22 [actionSources]="actionSources"
24 [isDataEnabled]="isDataEnabled" 23 [isDataEnabled]="isDataEnabled"
@@ -28,7 +28,7 @@ import { IAliasController } from '@core/api/widget-api.models'; @@ -28,7 +28,7 @@ import { IAliasController } from '@core/api/widget-api.models';
28 import { Widget, WidgetActionSource, WidgetTypeParameters } from '@shared/models/widget.models'; 28 import { Widget, WidgetActionSource, WidgetTypeParameters } from '@shared/models/widget.models';
29 import { WidgetComponentService } from '@home/components/widget/widget-component.service'; 29 import { WidgetComponentService } from '@home/components/widget/widget-component.service';
30 import { WidgetConfigComponentData } from '../../models/widget-component.models'; 30 import { WidgetConfigComponentData } from '../../models/widget-component.models';
31 -import { isString } from '@core/utils'; 31 +import { isDefined, isString } from '@core/utils';
32 import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 32 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
33 33
34 @Component({ 34 @Component({
@@ -92,13 +92,14 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan @@ -92,13 +92,14 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
92 const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget); 92 const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget);
93 this.widgetConfig = { 93 this.widgetConfig = {
94 config: this.widget.config, 94 config: this.widget.config,
95 - layout: this.widgetLayout 95 + layout: this.widgetLayout,
  96 + widgetType: this.widget.type
96 }; 97 };
97 const settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; 98 const settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
98 const dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; 99 const dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
99 this.typeParameters = widgetInfo.typeParameters; 100 this.typeParameters = widgetInfo.typeParameters;
100 this.actionSources = widgetInfo.actionSources; 101 this.actionSources = widgetInfo.actionSources;
101 - this.isDataEnabled = widgetInfo.typeParameters && !widgetInfo.typeParameters.useCustomDatasources; 102 + this.isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
102 if (!settingsSchema || settingsSchema === '') { 103 if (!settingsSchema || settingsSchema === '') {
103 this.settingsSchema = {}; 104 this.settingsSchema = {};
104 } else { 105 } else {
@@ -23,10 +23,5 @@ @@ -23,10 +23,5 @@
23 padding-top: 0; 23 padding-top: 0;
24 } 24 }
25 } 25 }
26 - .mat-form-field-flex {  
27 - .mat-form-field-infix {  
28 - border-top: 0;  
29 - }  
30 - }  
31 } 26 }
32 } 27 }
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host ::ng-deep {
  17 + .mat-form-field {
  18 + .mat-form-field-infix {
  19 + border-top: none;
  20 + }
  21 + }
  22 +}
@@ -20,8 +20,8 @@ import { @@ -20,8 +20,8 @@ import {
20 Component, 20 Component,
21 ElementRef, 21 ElementRef,
22 forwardRef, 22 forwardRef,
23 - Input,  
24 - OnInit, 23 + Input, OnChanges,
  24 + OnInit, SimpleChanges,
25 SkipSelf, 25 SkipSelf,
26 ViewChild 26 ViewChild
27 } from '@angular/core'; 27 } from '@angular/core';
@@ -49,7 +49,7 @@ import { emptyPageData } from '@shared/models/page/page-data'; @@ -49,7 +49,7 @@ import { emptyPageData } from '@shared/models/page/page-data';
49 @Component({ 49 @Component({
50 selector: 'tb-entity-list', 50 selector: 'tb-entity-list',
51 templateUrl: './entity-list.component.html', 51 templateUrl: './entity-list.component.html',
52 - styleUrls: [], 52 + styleUrls: ['./entity-list.component.scss'],
53 providers: [ 53 providers: [
54 { 54 {
55 provide: NG_VALUE_ACCESSOR, 55 provide: NG_VALUE_ACCESSOR,
@@ -58,21 +58,14 @@ import { emptyPageData } from '@shared/models/page/page-data'; @@ -58,21 +58,14 @@ import { emptyPageData } from '@shared/models/page/page-data';
58 } 58 }
59 ] 59 ]
60 }) 60 })
61 -export class EntityListComponent implements ControlValueAccessor, OnInit, AfterViewInit { 61 +export class EntityListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges {
62 62
63 entityListFormGroup: FormGroup; 63 entityListFormGroup: FormGroup;
64 64
65 modelValue: Array<string> | null; 65 modelValue: Array<string> | null;
66 66
67 - entityTypeValue: EntityType;  
68 -  
69 @Input() 67 @Input()
70 - set entityType(entityType: EntityType) {  
71 - if (this.entityTypeValue !== entityType) {  
72 - this.entityTypeValue = entityType;  
73 - this.reset();  
74 - }  
75 - } 68 + entityType: EntityType;
76 69
77 private requiredValue: boolean; 70 private requiredValue: boolean;
78 get required(): boolean { 71 get required(): boolean {
@@ -143,6 +136,17 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV @@ -143,6 +136,17 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
143 ); 136 );
144 } 137 }
145 138
  139 + ngOnChanges(changes: SimpleChanges): void {
  140 + for (const propName of Object.keys(changes)) {
  141 + const change = changes[propName];
  142 + if (!change.firstChange && change.currentValue !== change.previousValue) {
  143 + if (propName === 'entityType') {
  144 + this.reset();
  145 + }
  146 + }
  147 + }
  148 + }
  149 +
146 ngAfterViewInit(): void { 150 ngAfterViewInit(): void {
147 } 151 }
148 152
@@ -159,7 +163,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV @@ -159,7 +163,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
159 this.searchText = ''; 163 this.searchText = '';
160 if (value != null && value.length > 0) { 164 if (value != null && value.length > 0) {
161 this.modelValue = [...value]; 165 this.modelValue = [...value];
162 - this.entityService.getEntities(this.entityTypeValue, value).subscribe( 166 + this.entityService.getEntities(this.entityType, value).subscribe(
163 (entities) => { 167 (entities) => {
164 this.entities = entities; 168 this.entities = entities;
165 this.entityListFormGroup.get('entities').setValue(this.entities); 169 this.entityListFormGroup.get('entities').setValue(this.entities);
@@ -218,7 +222,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV @@ -218,7 +222,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
218 222
219 fetchEntities(searchText?: string): Observable<Array<BaseData<EntityId>>> { 223 fetchEntities(searchText?: string): Observable<Array<BaseData<EntityId>>> {
220 this.searchText = searchText; 224 this.searchText = searchText;
221 - return this.entityService.getEntitiesByNameFilter(this.entityTypeValue, searchText, 225 + return this.entityService.getEntitiesByNameFilter(this.entityType, searchText,
222 50, '', false, true).pipe( 226 50, '', false, true).pipe(
223 map((data) => data ? data : [])); 227 map((data) => data ? data : []));
224 } 228 }
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<mat-form-field appearance="standard" [formGroup]="entitySubtypeListFormGroup" class="mat-block">
  19 + <mat-chip-list #chipList formControlName="entitySubtypeList">
  20 + <mat-chip
  21 + *ngFor="let entitySubtype of entitySubtypeList"
  22 + [selectable]="!disabled"
  23 + [removable]="!disabled"
  24 + (removed)="remove(entitySubtype)">
  25 + {{entitySubtype}}
  26 + <mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
  27 + </mat-chip>
  28 + <input matInput type="text" placeholder="{{ !disabled ? ((!entitySubtypeList || !entitySubtypeList.length) ? placeholder : secondaryPlaceholder) : '' }}"
  29 + style="max-width: 200px;"
  30 + #entitySubtypeInput
  31 + (focusin)="onFocus()"
  32 + formControlName="entitySubtype"
  33 + matAutocompleteOrigin
  34 + #origin="matAutocompleteOrigin"
  35 + [matAutocompleteConnectedTo]="origin"
  36 + [matAutocomplete]="entitySubtypeAutocomplete"
  37 + [matChipInputFor]="chipList"
  38 + [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
  39 + (matChipInputTokenEnd)="chipAdd($event)">
  40 + </mat-chip-list>
  41 + <mat-autocomplete #entitySubtypeAutocomplete="matAutocomplete"
  42 + class="tb-autocomplete"
  43 + (optionSelected)="selected($event)"
  44 + [displayWith]="displayEntitySubtypeFn">
  45 + <mat-option *ngFor="let entitySubtype of filteredEntitySubtypeList | async" [value]="entitySubtype">
  46 + <span [innerHTML]="entitySubtype | highlight:searchText"></span>
  47 + </mat-option>
  48 + <mat-option *ngIf="!(filteredEntitySubtypeList | async)?.length" [value]="null">
  49 + <span>
  50 + {{ translate.get(noSubtypesMathingText, {entitySubtype: searchText}) | async }}
  51 + </span>
  52 + </mat-option>
  53 + </mat-autocomplete>
  54 + <mat-error *ngIf="entitySubtypeListFormGroup.get('entitySubtypeList').hasError('required')">
  55 + {{ subtypeListEmptyText | translate }}
  56 + </mat-error>
  57 +</mat-form-field>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host ::ng-deep {
  17 + .mat-form-field {
  18 + .mat-form-field-infix {
  19 + border-top: none;
  20 + }
  21 + }
  22 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + AfterContentInit,
  19 + AfterViewInit,
  20 + Component,
  21 + ElementRef,
  22 + forwardRef,
  23 + Input, OnDestroy,
  24 + OnInit,
  25 + SkipSelf,
  26 + ViewChild
  27 +} from '@angular/core';
  28 +import {
  29 + ControlValueAccessor,
  30 + FormBuilder,
  31 + FormControl,
  32 + FormGroup,
  33 + FormGroupDirective,
  34 + NG_VALUE_ACCESSOR, NgForm, Validators
  35 +} from '@angular/forms';
  36 +import { Observable, of, Subscription, throwError } from 'rxjs';
  37 +import { map, mergeMap, startWith, tap, share, pairwise, filter, publishReplay, refCount } from 'rxjs/operators';
  38 +import {Store} from '@ngrx/store';
  39 +import {AppState} from '@app/core/core.state';
  40 +import {TranslateService} from '@ngx-translate/core';
  41 +import { AliasEntityType, EntitySubtype, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
  42 +import {BaseData} from '@shared/models/base-data';
  43 +import {EntityId} from '@shared/models/id/entity-id';
  44 +import {EntityService} from '@core/http/entity.service';
  45 +import {ErrorStateMatcher, MatAutocomplete, MatAutocompleteSelectedEvent, MatChipList} from '@angular/material';
  46 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  47 +import { emptyPageData } from '@shared/models/page/page-data';
  48 +import { AssetService } from '@core/http/asset.service';
  49 +import { DeviceService } from '@core/http/device.service';
  50 +import { EntityViewService } from '@core/http/entity-view.service';
  51 +import { BroadcastService } from '@core/services/broadcast.service';
  52 +import { MatChipInputEvent } from '@angular/material/chips';
  53 +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
  54 +
  55 +@Component({
  56 + selector: 'tb-entity-subtype-list',
  57 + templateUrl: './entity-subtype-list.component.html',
  58 + styleUrls: ['./entity-subtype-list.component.scss'],
  59 + providers: [
  60 + {
  61 + provide: NG_VALUE_ACCESSOR,
  62 + useExisting: forwardRef(() => EntitySubTypeListComponent),
  63 + multi: true
  64 + }
  65 + ]
  66 +})
  67 +export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy {
  68 +
  69 + entitySubtypeListFormGroup: FormGroup;
  70 +
  71 + modelValue: Array<string> | null;
  72 +
  73 + private requiredValue: boolean;
  74 + get required(): boolean {
  75 + return this.requiredValue;
  76 + }
  77 + @Input()
  78 + set required(value: boolean) {
  79 + const newVal = coerceBooleanProperty(value);
  80 + if (this.requiredValue !== newVal) {
  81 + this.requiredValue = newVal;
  82 + this.updateValidators();
  83 + }
  84 + }
  85 +
  86 + @Input()
  87 + disabled: boolean;
  88 +
  89 + @Input()
  90 + entityType: EntityType;
  91 +
  92 + @ViewChild('entitySubtypeInput', {static: false}) entitySubtypeInput: ElementRef<HTMLInputElement>;
  93 + @ViewChild('entitySubtypeAutocomplete', {static: false}) entitySubtypeAutocomplete: MatAutocomplete;
  94 + @ViewChild('chipList', {static: true}) chipList: MatChipList;
  95 +
  96 + entitySubtypeList: Array<string> = [];
  97 + filteredEntitySubtypeList: Observable<Array<string>>;
  98 + entitySubtypes: Observable<Array<string>>;
  99 +
  100 + private broadcastSubscription: Subscription;
  101 +
  102 + placeholder: string;
  103 + secondaryPlaceholder: string;
  104 + noSubtypesMathingText: string;
  105 + subtypeListEmptyText: string;
  106 +
  107 + separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
  108 +
  109 + private searchText = '';
  110 +
  111 + private dirty = false;
  112 +
  113 + private propagateChange = (v: any) => { };
  114 +
  115 + constructor(private store: Store<AppState>,
  116 + private broadcast: BroadcastService,
  117 + public translate: TranslateService,
  118 + private assetService: AssetService,
  119 + private deviceService: DeviceService,
  120 + private entityViewService: EntityViewService,
  121 + private fb: FormBuilder) {
  122 + this.entitySubtypeListFormGroup = this.fb.group({
  123 + entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []],
  124 + entitySubtype: [null]
  125 + });
  126 + }
  127 +
  128 + updateValidators() {
  129 + this.entitySubtypeListFormGroup.get('entitySubtypeList').setValidators(this.required ? [Validators.required] : []);
  130 + this.entitySubtypeListFormGroup.get('entitySubtypeList').updateValueAndValidity();
  131 + }
  132 +
  133 + registerOnChange(fn: any): void {
  134 + this.propagateChange = fn;
  135 + }
  136 +
  137 + registerOnTouched(fn: any): void {
  138 + }
  139 +
  140 + ngOnInit() {
  141 +
  142 + switch (this.entityType) {
  143 + case EntityType.ASSET:
  144 + this.placeholder = this.required ? this.translate.instant('asset.enter-asset-type')
  145 + : this.translate.instant('asset.any-asset');
  146 + this.secondaryPlaceholder = '+' + this.translate.instant('asset.asset-type');
  147 + this.noSubtypesMathingText = 'asset.no-asset-types-matching';
  148 + this.subtypeListEmptyText = 'asset.asset-type-list-empty';
  149 + this.broadcastSubscription = this.broadcast.on('assetSaved', () => {
  150 + this.entitySubtypes = null;
  151 + });
  152 + break;
  153 + case EntityType.DEVICE:
  154 + this.placeholder = this.required ? this.translate.instant('device.enter-device-type')
  155 + : this.translate.instant('device.any-device');
  156 + this.secondaryPlaceholder = '+' + this.translate.instant('device.device-type');
  157 + this.noSubtypesMathingText = 'device.no-device-types-matching';
  158 + this.subtypeListEmptyText = 'device.device-type-list-empty';
  159 + this.broadcastSubscription = this.broadcast.on('deviceSaved', () => {
  160 + this.entitySubtypes = null;
  161 + });
  162 + break;
  163 + case EntityType.ENTITY_VIEW:
  164 + this.placeholder = this.required ? this.translate.instant('entity-view.enter-entity-view-type')
  165 + : this.translate.instant('entity-view.any-entity-view');
  166 + this.secondaryPlaceholder = '+' + this.translate.instant('entity-view.entity-view-type');
  167 + this.noSubtypesMathingText = 'entity-view.no-entity-view-types-matching';
  168 + this.subtypeListEmptyText = 'entity-view.entity-view-type-list-empty';
  169 + this.broadcastSubscription = this.broadcast.on('entityViewSaved', () => {
  170 + this.entitySubtypes = null;
  171 + });
  172 + break;
  173 + }
  174 +
  175 + this.filteredEntitySubtypeList = this.entitySubtypeListFormGroup.get('entitySubtype').valueChanges
  176 + .pipe(
  177 + map(value => value ? value : ''),
  178 + mergeMap(name => this.fetchEntitySubtypes(name) ),
  179 + share()
  180 + );
  181 + }
  182 +
  183 + ngAfterViewInit(): void {
  184 + }
  185 +
  186 + ngOnDestroy(): void {
  187 + if (this.broadcastSubscription) {
  188 + this.broadcastSubscription.unsubscribe();
  189 + }
  190 + }
  191 +
  192 + setDisabledState(isDisabled: boolean): void {
  193 + this.disabled = isDisabled;
  194 + if (isDisabled) {
  195 + this.entitySubtypeListFormGroup.disable({emitEvent: false});
  196 + } else {
  197 + this.entitySubtypeListFormGroup.enable({emitEvent: false});
  198 + }
  199 + }
  200 +
  201 + writeValue(value: Array<string> | null): void {
  202 + this.searchText = '';
  203 + if (value != null && value.length > 0) {
  204 + this.modelValue = [...value];
  205 + this.entitySubtypeList = [...value];
  206 + this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList);
  207 + } else {
  208 + this.entitySubtypeList = [];
  209 + this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList);
  210 + this.modelValue = null;
  211 + }
  212 + this.dirty = true;
  213 + }
  214 +
  215 + add(entitySubtype: string): void {
  216 + if (!this.modelValue || this.modelValue.indexOf(entitySubtype) === -1) {
  217 + if (!this.modelValue) {
  218 + this.modelValue = [];
  219 + }
  220 + this.modelValue.push(entitySubtype);
  221 + this.entitySubtypeList.push(entitySubtype);
  222 + this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList);
  223 + }
  224 + this.propagateChange(this.modelValue);
  225 + this.clear();
  226 + }
  227 +
  228 + chipAdd(event: MatChipInputEvent): void {
  229 + const value = event.value;
  230 + if ((value || '').trim()) {
  231 + this.add(value.trim());
  232 + }
  233 + this.clear('');
  234 + }
  235 +
  236 + remove(entitySubtype: string) {
  237 + const index = this.entitySubtypeList.indexOf(entitySubtype);
  238 + if (index >= 0) {
  239 + this.entitySubtypeList.splice(index, 1);
  240 + this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList);
  241 + this.modelValue.splice(index, 1);
  242 + if (!this.modelValue.length) {
  243 + this.modelValue = null;
  244 + }
  245 + this.propagateChange(this.modelValue);
  246 + this.clear();
  247 + }
  248 + }
  249 +
  250 + selected(event: MatAutocompleteSelectedEvent): void {
  251 + this.add(event.option.viewValue);
  252 + this.clear('');
  253 + }
  254 +
  255 + displayEntitySubtypeFn(entitySubtype?: string): string | undefined {
  256 + return entitySubtype ? entitySubtype : undefined;
  257 + }
  258 +
  259 + fetchEntitySubtypes(searchText?: string): Observable<Array<string>> {
  260 + this.searchText = searchText;
  261 + return this.getEntitySubtypes().pipe(
  262 + map(subTypes => {
  263 + let result = subTypes.filter( subType => {
  264 + return searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true;
  265 + });
  266 + if (!result.length) {
  267 + result = [searchText];
  268 + }
  269 + return result;
  270 + })
  271 + );
  272 + }
  273 +
  274 + getEntitySubtypes(): Observable<Array<string>> {
  275 + if (!this.entitySubtypes) {
  276 + let subTypesObservable: Observable<Array<EntitySubtype>>;
  277 + switch (this.entityType) {
  278 + case EntityType.ASSET:
  279 + subTypesObservable = this.assetService.getAssetTypes(false, true);
  280 + break;
  281 + case EntityType.DEVICE:
  282 + subTypesObservable = this.deviceService.getDeviceTypes(false, true);
  283 + break;
  284 + case EntityType.ENTITY_VIEW:
  285 + subTypesObservable = this.entityViewService.getEntityViewTypes(false, true);
  286 + break;
  287 + }
  288 + if (subTypesObservable) {
  289 + this.entitySubtypes = subTypesObservable.pipe(
  290 + map(subTypes => subTypes.map(subType => subType.type)),
  291 + publishReplay(1),
  292 + refCount()
  293 + );
  294 + } else {
  295 + return throwError(null);
  296 + }
  297 + }
  298 + return this.entitySubtypes;
  299 + }
  300 +
  301 + onFocus() {
  302 + if (this.dirty) {
  303 + this.entitySubtypeListFormGroup.get('entitySubtype').updateValueAndValidity({onlySelf: true, emitEvent: true});
  304 + this.dirty = false;
  305 + }
  306 + }
  307 +
  308 + clear(value: string = '') {
  309 + this.entitySubtypeInput.nativeElement.value = value;
  310 + this.entitySubtypeListFormGroup.get('entitySubtype').patchValue(value, {emitEvent: true});
  311 + setTimeout(() => {
  312 + this.entitySubtypeInput.nativeElement.blur();
  313 + this.entitySubtypeInput.nativeElement.focus();
  314 + }, 0);
  315 + }
  316 +
  317 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<mat-form-field appearance="standard" [formGroup]="entityTypeListFormGroup" class="mat-block">
  19 + <mat-chip-list #chipList formControlName="entityTypeList">
  20 + <mat-chip
  21 + *ngFor="let entityType of entityTypeList"
  22 + [selectable]="!disabled"
  23 + [removable]="!disabled"
  24 + (removed)="remove(entityType)">
  25 + {{entityType.name}}
  26 + <mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
  27 + </mat-chip>
  28 + <input matInput type="text" placeholder="{{ !disabled ? ((!entityTypeList || !entityTypeList.length) ? placeholder : secondaryPlaceholder) : '' }}"
  29 + style="max-width: 200px;"
  30 + #entityTypeInput
  31 + (focusin)="onFocus()"
  32 + formControlName="entityType"
  33 + matAutocompleteOrigin
  34 + #origin="matAutocompleteOrigin"
  35 + [matAutocompleteConnectedTo]="origin"
  36 + [matAutocomplete]="entityTypeAutocomplete"
  37 + [matChipInputFor]="chipList">
  38 + </mat-chip-list>
  39 + <mat-autocomplete #entityTypeAutocomplete="matAutocomplete"
  40 + class="tb-autocomplete"
  41 + [displayWith]="displayEntityTypeFn">
  42 + <mat-option *ngFor="let entityType of filteredEntityTypeList | async" [value]="entityType">
  43 + <span [innerHTML]="entityType.name | highlight:searchText"></span>
  44 + </mat-option>
  45 + <mat-option *ngIf="!(filteredEntityTypeList | async)?.length" [value]="null">
  46 + <span>
  47 + {{ translate.get('entity.no-entity-types-matching', {entityType: searchText}) | async }}
  48 + </span>
  49 + </mat-option>
  50 + </mat-autocomplete>
  51 + <mat-error *ngIf="entityTypeListFormGroup.get('entityTypeList').hasError('required')">
  52 + {{ 'entity.entity-type-list-empty' | translate }}
  53 + </mat-error>
  54 +</mat-form-field>
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + AfterContentInit,
  19 + AfterViewInit,
  20 + Component,
  21 + ElementRef,
  22 + forwardRef,
  23 + Input,
  24 + OnInit,
  25 + SkipSelf,
  26 + ViewChild
  27 +} from '@angular/core';
  28 +import {
  29 + ControlValueAccessor,
  30 + FormBuilder,
  31 + FormControl,
  32 + FormGroup,
  33 + FormGroupDirective,
  34 + NG_VALUE_ACCESSOR, NgForm, Validators
  35 +} from '@angular/forms';
  36 +import {Observable, of} from 'rxjs';
  37 +import {map, mergeMap, startWith, tap, share, pairwise, filter} from 'rxjs/operators';
  38 +import {Store} from '@ngrx/store';
  39 +import {AppState} from '@app/core/core.state';
  40 +import {TranslateService} from '@ngx-translate/core';
  41 +import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
  42 +import {BaseData} from '@shared/models/base-data';
  43 +import {EntityId} from '@shared/models/id/entity-id';
  44 +import {EntityService} from '@core/http/entity.service';
  45 +import {ErrorStateMatcher, MatAutocomplete, MatAutocompleteSelectedEvent, MatChipList} from '@angular/material';
  46 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  47 +import { emptyPageData } from '@shared/models/page/page-data';
  48 +
  49 +interface EntityTypeInfo {
  50 + name: string;
  51 + value: EntityType;
  52 +}
  53 +
  54 +@Component({
  55 + selector: 'tb-entity-type-list',
  56 + templateUrl: './entity-type-list.component.html',
  57 + styleUrls: [],
  58 + providers: [
  59 + {
  60 + provide: NG_VALUE_ACCESSOR,
  61 + useExisting: forwardRef(() => EntityTypeListComponent),
  62 + multi: true
  63 + }
  64 + ]
  65 +})
  66 +export class EntityTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit {
  67 +
  68 + entityTypeListFormGroup: FormGroup;
  69 +
  70 + modelValue: Array<EntityType> | null;
  71 +
  72 + private requiredValue: boolean;
  73 + get required(): boolean {
  74 + return this.requiredValue;
  75 + }
  76 + @Input()
  77 + set required(value: boolean) {
  78 + const newVal = coerceBooleanProperty(value);
  79 + if (this.requiredValue !== newVal) {
  80 + this.requiredValue = newVal;
  81 + this.updateValidators();
  82 + }
  83 + }
  84 +
  85 + @Input()
  86 + disabled: boolean;
  87 +
  88 + @Input()
  89 + allowedEntityTypes: Array<EntityType | AliasEntityType>;
  90 +
  91 + @Input()
  92 + ignoreAuthorityFilter: boolean;
  93 +
  94 + @ViewChild('entityTypeInput', {static: false}) entityTypeInput: ElementRef<HTMLInputElement>;
  95 + @ViewChild('entityTypeAutocomplete', {static: false}) entityTypeAutocomplete: MatAutocomplete;
  96 + @ViewChild('chipList', {static: true}) chipList: MatChipList;
  97 +
  98 + allEntityTypeList: Array<EntityTypeInfo> = [];
  99 + entityTypeList: Array<EntityTypeInfo> = [];
  100 + filteredEntityTypeList: Observable<Array<EntityTypeInfo>>;
  101 +
  102 + placeholder: string;
  103 + secondaryPlaceholder: string;
  104 +
  105 + private searchText = '';
  106 +
  107 + private dirty = false;
  108 +
  109 + private propagateChange = (v: any) => { };
  110 +
  111 + constructor(private store: Store<AppState>,
  112 + public translate: TranslateService,
  113 + private entityService: EntityService,
  114 + private fb: FormBuilder) {
  115 + this.entityTypeListFormGroup = this.fb.group({
  116 + entityTypeList: [this.entityTypeList, this.required ? [Validators.required] : []],
  117 + entityType: [null]
  118 + });
  119 + }
  120 +
  121 + updateValidators() {
  122 + this.entityTypeListFormGroup.get('entityTypeList').setValidators(this.required ? [Validators.required] : []);
  123 + this.entityTypeListFormGroup.get('entityTypeList').updateValueAndValidity();
  124 + }
  125 +
  126 + registerOnChange(fn: any): void {
  127 + this.propagateChange = fn;
  128 + }
  129 +
  130 + registerOnTouched(fn: any): void {
  131 + }
  132 +
  133 + ngOnInit() {
  134 +
  135 + this.placeholder = this.required ? this.translate.instant('entity.enter-entity-type')
  136 + : this.translate.instant('entity.any-entity');
  137 + this.secondaryPlaceholder = '+' + this.translate.instant('entity.entity-type');
  138 +
  139 + let entityTypes: Array<EntityType | AliasEntityType>;
  140 + if (this.ignoreAuthorityFilter && this.allowedEntityTypes
  141 + && this.allowedEntityTypes.length) {
  142 + entityTypes = [];
  143 + this.allowedEntityTypes.forEach((entityTypeValue) => {
  144 + entityTypes.push(entityTypeValue);
  145 + });
  146 + } else {
  147 + entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes) as Array<EntityType>;
  148 + }
  149 +
  150 + entityTypes.forEach((entityType) => {
  151 + this.allEntityTypeList.push({
  152 + name: this.translate.instant(entityTypeTranslations.get(entityType).type),
  153 + value: entityType as EntityType
  154 + });
  155 + });
  156 +
  157 + this.filteredEntityTypeList = this.entityTypeListFormGroup.get('entityType').valueChanges
  158 + .pipe(
  159 + // startWith<string | BaseData<EntityId>>(''),
  160 + tap((value) => {
  161 + if (value && typeof value !== 'string') {
  162 + this.add(value);
  163 + } else if (value === null) {
  164 + this.clear(this.entityTypeInput.nativeElement.value);
  165 + }
  166 + }),
  167 + filter((value) => typeof value === 'string'),
  168 + map((value) => value ? (typeof value === 'string' ? value : value.name) : ''),
  169 + mergeMap(name => this.fetchEntityTypes(name) ),
  170 + share()
  171 + );
  172 + }
  173 +
  174 + ngAfterViewInit(): void {
  175 + }
  176 +
  177 + setDisabledState(isDisabled: boolean): void {
  178 + this.disabled = isDisabled;
  179 + if (isDisabled) {
  180 + this.entityTypeListFormGroup.disable({emitEvent: false});
  181 + } else {
  182 + this.entityTypeListFormGroup.enable({emitEvent: false});
  183 + }
  184 + }
  185 +
  186 + writeValue(value: Array<EntityType> | null): void {
  187 + this.searchText = '';
  188 + if (value != null && value.length > 0) {
  189 + this.modelValue = [...value];
  190 + this.entityTypeList = [];
  191 + value.forEach((entityType) => {
  192 + this.entityTypeList.push({
  193 + name: this.translate.instant(entityTypeTranslations.get(entityType).type),
  194 + value: entityType
  195 + });
  196 + });
  197 + this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList);
  198 + } else {
  199 + this.entityTypeList = [];
  200 + this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList);
  201 + this.modelValue = null;
  202 + }
  203 + this.dirty = true;
  204 + }
  205 +
  206 + add(entityType: EntityTypeInfo): void {
  207 + if (!this.modelValue || this.modelValue.indexOf(entityType.value) === -1) {
  208 + if (!this.modelValue) {
  209 + this.modelValue = [];
  210 + }
  211 + this.modelValue.push(entityType.value);
  212 + this.entityTypeList.push(entityType);
  213 + this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList);
  214 + }
  215 + this.propagateChange(this.modelValue);
  216 + this.clear();
  217 + }
  218 +
  219 + remove(entityType: EntityTypeInfo) {
  220 + const index = this.entityTypeList.indexOf(entityType);
  221 + if (index >= 0) {
  222 + this.entityTypeList.splice(index, 1);
  223 + this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList);
  224 + this.modelValue.splice(index, 1);
  225 + if (!this.modelValue.length) {
  226 + this.modelValue = null;
  227 + }
  228 + this.propagateChange(this.modelValue);
  229 + this.clear();
  230 + }
  231 + }
  232 +
  233 + displayEntityTypeFn(entityType?: EntityTypeInfo): string | undefined {
  234 + return entityType ? entityType.name : undefined;
  235 + }
  236 +
  237 + fetchEntityTypes(searchText?: string): Observable<Array<EntityTypeInfo>> {
  238 + this.searchText = searchText;
  239 + let result = this.allEntityTypeList;
  240 + if (searchText && searchText.length) {
  241 + result = this.allEntityTypeList.filter((entityTypeInfo) => entityTypeInfo.name.toLowerCase().includes(searchText.toLowerCase()));
  242 + }
  243 + return of(result);
  244 + }
  245 +
  246 + onFocus() {
  247 + if (this.dirty) {
  248 + this.entityTypeListFormGroup.get('entityType').updateValueAndValidity({onlySelf: true, emitEvent: true});
  249 + this.dirty = false;
  250 + }
  251 + }
  252 +
  253 + clear(value: string = '') {
  254 + this.entityTypeInput.nativeElement.value = value;
  255 + this.entityTypeListFormGroup.get('entityType').patchValue(value, {emitEvent: true});
  256 + setTimeout(() => {
  257 + this.entityTypeInput.nativeElement.blur();
  258 + this.entityTypeInput.nativeElement.focus();
  259 + }, 0);
  260 + }
  261 +
  262 +}
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<mat-form-field [formGroup]="entityTypeFormGroup" class="mat-block"> 18 +<mat-form-field [formGroup]="entityTypeFormGroup">
19 <mat-label *ngIf="showLabel">{{ 'entity.type' | translate }}</mat-label> 19 <mat-label *ngIf="showLabel">{{ 'entity.type' | translate }}</mat-label>
20 <mat-select [required]="required" 20 <mat-select [required]="required"
21 class="tb-entity-type-select" matInput formControlName="entityType"> 21 class="tb-entity-type-select" matInput formControlName="entityType">
@@ -15,3 +15,13 @@ @@ -15,3 +15,13 @@
15 */ 15 */
16 :host { 16 :host {
17 } 17 }
  18 +
  19 +:host ::ng-deep {
  20 + .mat-form-field-infix {
  21 + width: 100%;
  22 + }
  23 + .mat-select-value {
  24 + max-width: 200px;
  25 + min-width: 100px;
  26 + }
  27 +}
@@ -45,8 +45,14 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, @@ -45,8 +45,14 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit,
45 @Input() 45 @Input()
46 useAliasEntityTypes: boolean; 46 useAliasEntityTypes: boolean;
47 47
  48 + private showLabelValue: boolean;
  49 + get showLabel(): boolean {
  50 + return this.showLabelValue;
  51 + }
48 @Input() 52 @Input()
49 - showLabel: boolean; 53 + set showLabel(value: boolean) {
  54 + this.showLabelValue = coerceBooleanProperty(value);
  55 + }
50 56
51 private requiredValue: boolean; 57 private requiredValue: boolean;
52 get required(): boolean { 58 get required(): boolean {
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Input } from '@angular/core';
  18 +import { trigger, state, style, transition, animate } from '@angular/animations';
  19 +
  20 +@Component({
  21 + selector: 'tb-error',
  22 + template: `
  23 + <div [@animation]="state" style="margin-top:0.5rem;font-size:.75rem">
  24 + <mat-error >
  25 + {{message}}
  26 + </mat-error>
  27 + </div>
  28 + `,
  29 + styles: [`
  30 + :host {
  31 + height: 24px;
  32 + }
  33 + `],
  34 + animations: [
  35 + trigger('animation', [
  36 + state('show', style({
  37 + opacity: 1,
  38 + })),
  39 + state('hide', style({
  40 + opacity: 0,
  41 + transform: 'translateY(-1rem)'
  42 + })),
  43 + transition('show => hide', animate('200ms ease-out')),
  44 + transition('* => show', animate('200ms ease-in'))
  45 +
  46 + ]),
  47 + ]
  48 +})
  49 +export class TbErrorComponent {
  50 + errorValue: any;
  51 + state: any;
  52 + message;
  53 +
  54 + @Input()
  55 + set error(value) {
  56 + if (value && !this.message) {
  57 + this.message = value;
  58 + this.state = 'hide';
  59 + setTimeout(() => {
  60 + this.state = 'show';
  61 + });
  62 + } else {
  63 + this.errorValue = value;
  64 + this.state = value ? 'show' : 'hide';
  65 + }
  66 + }
  67 +}
@@ -14,7 +14,10 @@ @@ -14,7 +14,10 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { EntityType } from '@shared/models/entity-type.models'; 17 +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
  18 +import { EntityId } from '@shared/models/id/entity-id';
  19 +import { EntitySearchDirection, EntityTypeFilter } from '@shared/models/relation.models';
  20 +import { EntityInfo } from './entity.models';
18 21
19 export enum AliasFilterType { 22 export enum AliasFilterType {
20 singleEntity = 'singleEntity', 23 singleEntity = 'singleEntity',
@@ -46,15 +49,87 @@ export const aliasFilterTypeTranslationMap = new Map<AliasFilterType, string>( @@ -46,15 +49,87 @@ export const aliasFilterTypeTranslationMap = new Map<AliasFilterType, string>(
46 ] 49 ]
47 ); 50 );
48 51
49 -export interface EntityAliasFilter {  
50 - type: AliasFilterType;  
51 - entityType: EntityType;  
52 - resolveMultiple: boolean; 52 +export interface SingleEntityFilter {
  53 + singleEntity?: EntityId;
  54 +}
  55 +
  56 +export interface EntityListFilter {
  57 + entityType?: EntityType;
53 entityList?: string[]; 58 entityList?: string[];
  59 +}
  60 +
  61 +export interface EntityNameFilter {
  62 + entityType?: EntityType;
54 entityNameFilter?: string; 63 entityNameFilter?: string;
55 - [key: string]: any;  
56 - // TODO: 64 +}
  65 +
  66 +export interface StateEntityFilter {
  67 + stateEntityParamName?: string;
  68 + defaultStateEntity?: EntityId;
  69 +}
  70 +
  71 +export interface AssetTypeFilter {
  72 + assetType?: string;
  73 + assetNameFilter?: string;
  74 +}
  75 +
  76 +export interface DeviceTypeFilter {
  77 + deviceType?: string;
  78 + deviceNameFilter?: string;
  79 +}
  80 +
  81 +export interface EntityViewFilter {
  82 + entityViewType?: string;
  83 + entityViewNameFilter?: string;
  84 +}
57 85
  86 +export interface RelationsQueryFilter {
  87 + rootStateEntity?: boolean;
  88 + stateEntityParamName?: string;
  89 + defaultStateEntity?: EntityId;
  90 + rootEntity?: EntityId;
  91 + direction?: EntitySearchDirection;
  92 + filters?: Array<EntityTypeFilter>;
  93 + maxLevel?: number;
  94 +}
  95 +
  96 +export interface EntitySearchQueryFilter {
  97 + rootStateEntity?: boolean;
  98 + stateEntityParamName?: string;
  99 + defaultStateEntity?: EntityId;
  100 + rootEntity?: EntityId;
  101 + relationType?: string;
  102 + direction?: EntitySearchDirection;
  103 +}
  104 +
  105 +export interface AssetSearchQueryFilter extends EntitySearchQueryFilter {
  106 + assetTypes?: string[];
  107 +}
  108 +
  109 +export interface DeviceSearchQueryFilter extends EntitySearchQueryFilter {
  110 + deviceTypes?: string[];
  111 +}
  112 +
  113 +export interface EntityViewSearchQueryFilter extends EntitySearchQueryFilter {
  114 + entityViewTypes?: string[];
  115 +}
  116 +
  117 +export type EntityFilters =
  118 + SingleEntityFilter &
  119 + EntityListFilter &
  120 + EntityNameFilter &
  121 + StateEntityFilter &
  122 + AssetTypeFilter &
  123 + DeviceTypeFilter &
  124 + EntityViewFilter &
  125 + RelationsQueryFilter &
  126 + AssetSearchQueryFilter &
  127 + DeviceSearchQueryFilter &
  128 + EntityViewSearchQueryFilter;
  129 +
  130 +export interface EntityAliasFilter extends EntityFilters {
  131 + type?: AliasFilterType;
  132 + resolveMultiple: boolean;
58 } 133 }
59 134
60 export interface EntityAlias { 135 export interface EntityAlias {
@@ -62,9 +137,14 @@ export interface EntityAlias { @@ -62,9 +137,14 @@ export interface EntityAlias {
62 alias: string; 137 alias: string;
63 filter: EntityAliasFilter; 138 filter: EntityAliasFilter;
64 [key: string]: any; 139 [key: string]: any;
65 - // TODO:  
66 } 140 }
67 141
68 export interface EntityAliases { 142 export interface EntityAliases {
69 [id: string]: EntityAlias; 143 [id: string]: EntityAlias;
70 } 144 }
  145 +
  146 +export interface EntityAliasFilterResult {
  147 + entities: Array<EntityInfo>;
  148 + stateEntity: boolean;
  149 + entityParamName?: string;
  150 +}
@@ -19,6 +19,7 @@ import {AssetId} from './id/asset-id'; @@ -19,6 +19,7 @@ import {AssetId} from './id/asset-id';
19 import {TenantId} from '@shared/models/id/tenant-id'; 19 import {TenantId} from '@shared/models/id/tenant-id';
20 import {CustomerId} from '@shared/models/id/customer-id'; 20 import {CustomerId} from '@shared/models/id/customer-id';
21 import {DeviceCredentialsId} from '@shared/models/id/device-credentials-id'; 21 import {DeviceCredentialsId} from '@shared/models/id/device-credentials-id';
  22 +import { EntitySearchQuery } from '@shared/models/relation.models';
22 23
23 export interface Asset extends BaseData<AssetId> { 24 export interface Asset extends BaseData<AssetId> {
24 tenantId: TenantId; 25 tenantId: TenantId;
@@ -32,3 +33,7 @@ export interface AssetInfo extends Asset { @@ -32,3 +33,7 @@ export interface AssetInfo extends Asset {
32 customerTitle: string; 33 customerTitle: string;
33 customerIsPublic: boolean; 34 customerIsPublic: boolean;
34 } 35 }
  36 +
  37 +export interface AssetSearchQuery extends EntitySearchQuery {
  38 + assetTypes: Array<string>;
  39 +}
@@ -19,6 +19,7 @@ import {DeviceId} from './id/device-id'; @@ -19,6 +19,7 @@ import {DeviceId} from './id/device-id';
19 import {TenantId} from '@shared/models/id/tenant-id'; 19 import {TenantId} from '@shared/models/id/tenant-id';
20 import {CustomerId} from '@shared/models/id/customer-id'; 20 import {CustomerId} from '@shared/models/id/customer-id';
21 import {DeviceCredentialsId} from '@shared/models/id/device-credentials-id'; 21 import {DeviceCredentialsId} from '@shared/models/id/device-credentials-id';
  22 +import { EntitySearchQuery } from '@shared/models/relation.models';
22 23
23 export interface Device extends BaseData<DeviceId> { 24 export interface Device extends BaseData<DeviceId> {
24 tenantId: TenantId; 25 tenantId: TenantId;
@@ -52,3 +53,7 @@ export interface DeviceCredentials extends BaseData<DeviceCredentialsId> { @@ -52,3 +53,7 @@ export interface DeviceCredentials extends BaseData<DeviceCredentialsId> {
52 credentialsId: string; 53 credentialsId: string;
53 credentialsValue: string; 54 credentialsValue: string;
54 } 55 }
  56 +
  57 +export interface DeviceSearchQuery extends EntitySearchQuery {
  58 + deviceTypes: Array<string>;
  59 +}
@@ -20,6 +20,7 @@ import {TenantId} from '@shared/models/id/tenant-id'; @@ -20,6 +20,7 @@ import {TenantId} from '@shared/models/id/tenant-id';
20 import {CustomerId} from '@shared/models/id/customer-id'; 20 import {CustomerId} from '@shared/models/id/customer-id';
21 import {EntityViewId} from '@shared/models/id/entity-view-id'; 21 import {EntityViewId} from '@shared/models/id/entity-view-id';
22 import {EntityId} from '@shared/models/id/entity-id'; 22 import {EntityId} from '@shared/models/id/entity-id';
  23 +import { EntitySearchQuery } from '@shared/models/relation.models';
23 24
24 export interface AttributesEntityView { 25 export interface AttributesEntityView {
25 cs: Array<string>; 26 cs: Array<string>;
@@ -48,3 +49,7 @@ export interface EntityViewInfo extends EntityView { @@ -48,3 +49,7 @@ export interface EntityViewInfo extends EntityView {
48 customerTitle: string; 49 customerTitle: string;
49 customerIsPublic: boolean; 50 customerIsPublic: boolean;
50 } 51 }
  52 +
  53 +export interface EntityViewSearchQuery extends EntitySearchQuery {
  54 + entityViewTypes: Array<string>;
  55 +}
@@ -62,8 +62,8 @@ export interface RelationsSearchParameters { @@ -62,8 +62,8 @@ export interface RelationsSearchParameters {
62 rootId: string; 62 rootId: string;
63 rootType: EntityType; 63 rootType: EntityType;
64 direction: EntitySearchDirection; 64 direction: EntitySearchDirection;
65 - relationTypeGroup: RelationTypeGroup;  
66 - maxLevel: number; 65 + relationTypeGroup?: RelationTypeGroup;
  66 + maxLevel?: number;
67 } 67 }
68 68
69 export interface EntityRelationsQuery { 69 export interface EntityRelationsQuery {
@@ -71,6 +71,11 @@ export interface EntityRelationsQuery { @@ -71,6 +71,11 @@ export interface EntityRelationsQuery {
71 filters: Array<EntityTypeFilter>; 71 filters: Array<EntityTypeFilter>;
72 } 72 }
73 73
  74 +export interface EntitySearchQuery {
  75 + parameters: RelationsSearchParameters;
  76 + relationType: string;
  77 +}
  78 +
74 export interface EntityRelation { 79 export interface EntityRelation {
75 from: EntityId; 80 from: EntityId;
76 to: EntityId; 81 to: EntityId;
@@ -96,6 +96,9 @@ import { DashboardSelectPanelComponent } from '@shared/components/dashboard-sele @@ -96,6 +96,9 @@ import { DashboardSelectPanelComponent } from '@shared/components/dashboard-sele
96 import { DashboardSelectComponent } from '@shared/components/dashboard-select.component'; 96 import { DashboardSelectComponent } from '@shared/components/dashboard-select.component';
97 import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component'; 97 import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component';
98 import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe'; 98 import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe';
  99 +import { TbErrorComponent } from './components/tb-error.component';
  100 +import { EntityTypeListComponent } from './components/entity/entity-type-list.component';
  101 +import { EntitySubTypeListComponent } from './components/entity/entity-subtype-list.component';
99 102
100 @NgModule({ 103 @NgModule({
101 providers: [ 104 providers: [
@@ -122,6 +125,7 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe'; @@ -122,6 +125,7 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe';
122 HelpComponent, 125 HelpComponent,
123 TbCheckboxComponent, 126 TbCheckboxComponent,
124 TbSnackBarComponent, 127 TbSnackBarComponent,
  128 + TbErrorComponent,
125 BreadcrumbComponent, 129 BreadcrumbComponent,
126 UserMenuComponent, 130 UserMenuComponent,
127 TimewindowComponent, 131 TimewindowComponent,
@@ -135,12 +139,14 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe'; @@ -135,12 +139,14 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe';
135 DashboardAutocompleteComponent, 139 DashboardAutocompleteComponent,
136 EntitySubTypeAutocompleteComponent, 140 EntitySubTypeAutocompleteComponent,
137 EntitySubTypeSelectComponent, 141 EntitySubTypeSelectComponent,
  142 + EntitySubTypeListComponent,
138 EntityAutocompleteComponent, 143 EntityAutocompleteComponent,
139 EntityListComponent, 144 EntityListComponent,
140 EntityTypeSelectComponent, 145 EntityTypeSelectComponent,
141 EntitySelectComponent, 146 EntitySelectComponent,
142 EntityKeysListComponent, 147 EntityKeysListComponent,
143 EntityListSelectComponent, 148 EntityListSelectComponent,
  149 + EntityTypeListComponent,
144 RelationTypeAutocompleteComponent, 150 RelationTypeAutocompleteComponent,
145 SocialSharePanelComponent, 151 SocialSharePanelComponent,
146 JsonObjectEditComponent, 152 JsonObjectEditComponent,
@@ -207,6 +213,7 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe'; @@ -207,6 +213,7 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe';
207 TbAnchorComponent, 213 TbAnchorComponent,
208 HelpComponent, 214 HelpComponent,
209 TbCheckboxComponent, 215 TbCheckboxComponent,
  216 + TbErrorComponent,
210 BreadcrumbComponent, 217 BreadcrumbComponent,
211 UserMenuComponent, 218 UserMenuComponent,
212 TimewindowComponent, 219 TimewindowComponent,
@@ -218,12 +225,14 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe'; @@ -218,12 +225,14 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe';
218 DashboardAutocompleteComponent, 225 DashboardAutocompleteComponent,
219 EntitySubTypeAutocompleteComponent, 226 EntitySubTypeAutocompleteComponent,
220 EntitySubTypeSelectComponent, 227 EntitySubTypeSelectComponent,
  228 + EntitySubTypeListComponent,
221 EntityAutocompleteComponent, 229 EntityAutocompleteComponent,
222 EntityListComponent, 230 EntityListComponent,
223 EntityTypeSelectComponent, 231 EntityTypeSelectComponent,
224 EntitySelectComponent, 232 EntitySelectComponent,
225 EntityKeysListComponent, 233 EntityKeysListComponent,
226 EntityListSelectComponent, 234 EntityListSelectComponent,
  235 + EntityTypeListComponent,
227 RelationTypeAutocompleteComponent, 236 RelationTypeAutocompleteComponent,
228 SocialSharePanelComponent, 237 SocialSharePanelComponent,
229 JsonObjectEditComponent, 238 JsonObjectEditComponent,
@@ -246,6 +246,15 @@ div { @@ -246,6 +246,15 @@ div {
246 color: #808080; 246 color: #808080;
247 } 247 }
248 248
  249 +.mat-caption {
  250 + &.tb-required::after {
  251 + font-size: 10px;
  252 + color: rgba(0, 0, 0, .54);
  253 + vertical-align: top;
  254 + content: " *";
  255 + }
  256 +}
  257 +
249 pre.tb-highlight { 258 pre.tb-highlight {
250 display: block; 259 display: block;
251 padding: 15px; 260 padding: 15px;
@@ -303,6 +312,15 @@ pre.tb-highlight { @@ -303,6 +312,15 @@ pre.tb-highlight {
303 margin-top: 32px; 312 margin-top: 32px;
304 } 313 }
305 314
  315 +.tb-prompt {
  316 + display: flex;
  317 + font-size: 18px;
  318 + font-weight: 400;
  319 + line-height: 18px;
  320 + color: rgba(0, 0, 0, .38);
  321 + text-transform: uppercase;
  322 +}
  323 +
306 .tb-fullscreen { 324 .tb-fullscreen {
307 position: fixed !important; 325 position: fixed !important;
308 top: 0; 326 top: 0;