Commit d987c864564cc5fc9dee6b47e85389ccf0139d2e
1 parent
3cee4174
UI: Widget Config. Entity aliases dialog.
Showing
53 changed files
with
3607 additions
and
106 deletions
... | ... | @@ -1622,7 +1622,6 @@ |
1622 | 1622 | "version": "1.0.10", |
1623 | 1623 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", |
1624 | 1624 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", |
1625 | - "dev": true, | |
1626 | 1625 | "requires": { |
1627 | 1626 | "sprintf-js": "~1.0.2" |
1628 | 1627 | } |
... | ... | @@ -5308,9 +5307,9 @@ |
5308 | 5307 | "dev": true |
5309 | 5308 | }, |
5310 | 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 | 5313 | "dev": true, |
5315 | 5314 | "requires": { |
5316 | 5315 | "neo-async": "^2.6.0", |
... | ... | @@ -6532,6 +6531,14 @@ |
6532 | 6531 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", |
6533 | 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 | 6542 | "json-schema-traverse": { |
6536 | 6543 | "version": "0.4.1", |
6537 | 6544 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", |
... | ... | @@ -9989,8 +9996,7 @@ |
9989 | 9996 | "sprintf-js": { |
9990 | 9997 | "version": "1.0.3", |
9991 | 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 | 10001 | "sshpk": { |
9996 | 10002 | "version": "1.16.1", | ... | ... |
... | ... | @@ -21,7 +21,8 @@ import {HttpClient} from '@angular/common/http'; |
21 | 21 | import {PageLink} from '@shared/models/page/page-link'; |
22 | 22 | import {PageData} from '@shared/models/page/page-data'; |
23 | 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 | 27 | @Injectable({ |
27 | 28 | providedIn: 'root' |
... | ... | @@ -81,4 +82,9 @@ export class AssetService { |
81 | 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 | 23 | import { Tenant } from '@shared/models/tenant.model'; |
24 | 24 | import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models'; |
25 | 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 | 27 | import {EntitySubtype} from '@app/shared/models/entity-type.models'; |
28 | 28 | import {AuthService} from '../auth/auth.service'; |
29 | 29 | |
... | ... | @@ -115,4 +115,9 @@ export class DeviceService { |
115 | 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 | 21 | import {PageLink} from '@shared/models/page/page-link'; |
22 | 22 | import {PageData} from '@shared/models/page/page-data'; |
23 | 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 | 27 | @Injectable({ |
27 | 28 | providedIn: 'root' |
... | ... | @@ -80,4 +81,9 @@ export class EntityViewService { |
80 | 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 | 33 | import { AppState } from '@core/core.state'; |
34 | 34 | import { Authority } from '@shared/models/authority.enum'; |
35 | 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 | 37 | import { Customer } from '@app/shared/models/customer.model'; |
38 | 38 | import { AssetService } from '@core/http/asset.service'; |
39 | 39 | import { EntityViewService } from '@core/http/entity-view.service'; |
40 | 40 | import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
41 | 41 | import { defaultHttpOptions } from '@core/http/http-utils'; |
42 | 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 | 44 | import { Datasource, DatasourceType, KeyInfo } from '@app/shared/models/widget.models'; |
45 | 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 | 60 | @Injectable({ |
48 | 61 | providedIn: 'root' |
... | ... | @@ -60,6 +73,7 @@ export class EntityService { |
60 | 73 | private userService: UserService, |
61 | 74 | private ruleChainService: RuleChainService, |
62 | 75 | private dashboardService: DashboardService, |
76 | + private entityRelationService: EntityRelationService, | |
63 | 77 | private utils: UtilsService |
64 | 78 | ) { } |
65 | 79 | |
... | ... | @@ -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 | 419 | public prepareAllowedEntityTypesList(allowedEntityTypes: Array<EntityType | AliasEntityType>, |
352 | - useAliasEntityTypes: boolean): Array<EntityType | AliasEntityType> { | |
420 | + useAliasEntityTypes?: boolean): Array<EntityType | AliasEntityType> { | |
353 | 421 | const authUser = getCurrentAuthUser(this.store); |
354 | 422 | const entityTypes: Array<EntityType | AliasEntityType> = []; |
355 | 423 | switch (authUser.authority) { |
... | ... | @@ -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 | 794 | private createDatasourcesFromSubscriptionInfo(subscriptionInfo: SubscriptionInfo): Observable<Array<Datasource>> { |
448 | 795 | subscriptionInfo = this.validateSubscriptionInfo(subscriptionInfo); |
449 | 796 | if (subscriptionInfo.type === DatasourceType.entity) { | ... | ... |
... | ... | @@ -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 | 292 | private validateAndUpdateEntityAliases(configuration: DashboardConfiguration, |
282 | 293 | datasourcesByAliasId: {[aliasId: string]: Array<Datasource>}, |
283 | 294 | targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration { | ... | ... |
... | ... | @@ -17,19 +17,39 @@ |
17 | 17 | import { Inject, Injectable } from '@angular/core'; |
18 | 18 | import { WINDOW } from '@core/services/window.service'; |
19 | 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 | 21 | import { WindowMessage } from '@shared/models/window-message.model'; |
22 | 22 | import { TranslateService } from '@ngx-translate/core'; |
23 | 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 | 25 | import { EntityType } from '@shared/models/entity-type.models'; |
26 | 26 | import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models'; |
27 | 27 | import { alarmFields } from '@shared/models/alarm.models'; |
28 | 28 | import { materialColors } from '@app/shared/models/material.models'; |
29 | 29 | import { WidgetInfo } from '@home/models/widget-component.models'; |
30 | +import jsonSchemaDefaults from 'json-schema-defaults'; | |
30 | 31 | |
31 | 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 | 53 | @Injectable({ |
34 | 54 | providedIn: 'root' |
35 | 55 | }) |
... | ... | @@ -39,6 +59,22 @@ export class UtilsService { |
39 | 59 | widgetEditMode = false; |
40 | 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 | 78 | constructor(@Inject(WINDOW) private window: Window, |
43 | 79 | private translate: TranslateService) { |
44 | 80 | let frame: Element = null; |
... | ... | @@ -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 | 118 | public hashCode(str: string): number { |
61 | 119 | let hash = 0; |
62 | 120 | let i: number; |
... | ... | @@ -192,7 +250,7 @@ export class UtilsService { |
192 | 250 | return datasources; |
193 | 251 | } |
194 | 252 | |
195 | - public getMaterialColor(index) { | |
253 | + public getMaterialColor(index: number) { | |
196 | 254 | const colorIndex = index % materialColors.length; |
197 | 255 | return materialColors[colorIndex].value; |
198 | 256 | } | ... | ... |
... | ... | @@ -103,6 +103,23 @@ export function isString(value: any): boolean { |
103 | 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 | 123 | export function objToBase64(obj: any): string { |
107 | 124 | const json = JSON.stringify(obj); |
108 | 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 | +} | |
\ No newline at end of file | ... | ... |
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 | 41 | import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component'; |
42 | 42 | import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component'; |
43 | 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 | 50 | @NgModule({ |
46 | 51 | entryComponents: [ |
... | ... | @@ -52,7 +57,9 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com |
52 | 57 | AlarmDetailsDialogComponent, |
53 | 58 | AddAttributeDialogComponent, |
54 | 59 | EditAttributeValuePanelComponent, |
55 | - AliasesEntitySelectPanelComponent | |
60 | + AliasesEntitySelectPanelComponent, | |
61 | + EntityAliasesDialogComponent, | |
62 | + EntityAliasDialogComponent | |
56 | 63 | ], |
57 | 64 | declarations: |
58 | 65 | [ |
... | ... | @@ -67,6 +74,7 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com |
67 | 74 | EventTableComponent, |
68 | 75 | RelationTableComponent, |
69 | 76 | RelationDialogComponent, |
77 | + RelationFiltersComponent, | |
70 | 78 | AlarmTableHeaderComponent, |
71 | 79 | AlarmTableComponent, |
72 | 80 | AlarmDetailsDialogComponent, |
... | ... | @@ -75,10 +83,14 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com |
75 | 83 | EditAttributeValuePanelComponent, |
76 | 84 | AliasesEntitySelectPanelComponent, |
77 | 85 | AliasesEntitySelectComponent, |
86 | + EntityAliasesDialogComponent, | |
87 | + EntityAliasDialogComponent, | |
78 | 88 | DashboardComponent, |
79 | 89 | WidgetComponent, |
80 | 90 | LegendComponent, |
81 | - WidgetConfigComponent | |
91 | + WidgetConfigComponent, | |
92 | + EntityFilterViewComponent, | |
93 | + EntityFilterComponent | |
82 | 94 | ], |
83 | 95 | imports: [ |
84 | 96 | CommonModule, |
... | ... | @@ -93,14 +105,19 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com |
93 | 105 | AuditLogTableComponent, |
94 | 106 | EventTableComponent, |
95 | 107 | RelationTableComponent, |
108 | + RelationFiltersComponent, | |
96 | 109 | AlarmTableComponent, |
97 | 110 | AlarmDetailsDialogComponent, |
98 | 111 | AttributeTableComponent, |
99 | 112 | AliasesEntitySelectComponent, |
113 | + EntityAliasesDialogComponent, | |
114 | + EntityAliasDialogComponent, | |
100 | 115 | DashboardComponent, |
101 | 116 | WidgetComponent, |
102 | 117 | LegendComponent, |
103 | - WidgetConfigComponent | |
118 | + WidgetConfigComponent, | |
119 | + EntityFilterViewComponent, | |
120 | + EntityFilterComponent | |
104 | 121 | ], |
105 | 122 | providers: [ |
106 | 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;"> </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 | 16 | |
17 | 17 | --> |
18 | 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 | 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 | 26 | {{ 'widget-config.use-dashboard-timewindow' | translate }} |
27 | 27 | </mat-checkbox> |
28 | - <mat-checkbox [disabled]="useDashboardTimewindow" fxFlex [(ngModel)]="displayTimewindow" (ngModelChange)="updateModel()"> | |
28 | + <mat-checkbox fxFlex | |
29 | + formControlName="displayTimewindow"> | |
29 | 30 | {{ 'widget-config.display-timewindow' | translate }} |
30 | 31 | </mat-checkbox> |
31 | 32 | </div> |
32 | 33 | <section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;"> |
33 | 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 | 36 | aggregation="{{ widgetType === widgetTypes.timeseries }}" |
36 | - fxFlex [(ngModel)]="timewindow" (ngModelChange)="updateModel()"></tb-timewindow> | |
37 | + fxFlex formControlName="timewindow"></tb-timewindow> | |
37 | 38 | </section> |
38 | 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 | 134 | </div> |
40 | 135 | </mat-tab> |
41 | 136 | <mat-tab label="{{ 'widget-config.settings' | translate }}"> | ... | ... |
... | ... | @@ -19,20 +19,35 @@ import { PageComponent } from '@shared/components/page.component'; |
19 | 19 | import { Store } from '@ngrx/store'; |
20 | 20 | import { AppState } from '@core/core.state'; |
21 | 21 | import { |
22 | + DataKey, | |
22 | 23 | Datasource, |
24 | + DatasourceType, | |
23 | 25 | LegendConfig, |
24 | 26 | WidgetActionDescriptor, |
25 | - WidgetActionSource, WidgetConfigSettings, | |
27 | + WidgetActionSource, | |
28 | + WidgetConfigSettings, | |
26 | 29 | widgetType, |
27 | 30 | WidgetTypeParameters |
28 | 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 | 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 | 46 | import { IAliasController } from '@core/api/widget-api.models'; |
35 | 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 | 52 | @Component({ |
38 | 53 | selector: 'tb-widget-config', |
... | ... | @@ -55,6 +70,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
55 | 70 | |
56 | 71 | widgetTypes = widgetType; |
57 | 72 | |
73 | + alarmSearchStatuses = Object.keys(AlarmSearchStatus); | |
74 | + | |
58 | 75 | @Input() |
59 | 76 | forceExpandDatasources: boolean; |
60 | 77 | |
... | ... | @@ -62,9 +79,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
62 | 79 | isDataEnabled: boolean; |
63 | 80 | |
64 | 81 | @Input() |
65 | - widgetType: widgetType; | |
66 | - | |
67 | - @Input() | |
68 | 82 | typeParameters: WidgetTypeParameters; |
69 | 83 | |
70 | 84 | @Input() |
... | ... | @@ -84,6 +98,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
84 | 98 | |
85 | 99 | @Input() disabled: boolean; |
86 | 100 | |
101 | + widgetType: widgetType; | |
102 | + | |
87 | 103 | selectedTab: number; |
88 | 104 | title: string; |
89 | 105 | showTitleIcon: boolean; |
... | ... | @@ -101,17 +117,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
101 | 117 | titleStyle: string; |
102 | 118 | units: string; |
103 | 119 | decimals: number; |
104 | - useDashboardTimewindow: boolean; | |
105 | - displayTimewindow: boolean; | |
106 | - timewindow: Timewindow; | |
107 | 120 | showLegend: boolean; |
108 | 121 | legendConfig: LegendConfig; |
109 | 122 | actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; |
110 | - datasources: Array<Datasource>; | |
111 | 123 | targetDeviceAlias: EntityAlias; |
112 | 124 | alarmSource: Datasource; |
113 | - alarmSearchStatus: AlarmSearchStatus; | |
114 | - alarmsPollingInterval: number; | |
115 | 125 | settings: WidgetConfigSettings; |
116 | 126 | mobileOrder: number; |
117 | 127 | mobileHeight: number; |
... | ... | @@ -138,11 +148,51 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
138 | 148 | |
139 | 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 | 157 | super(store); |
143 | 158 | } |
144 | 159 | |
145 | 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 | 198 | registerOnChange(fn: any): void { |
... | ... | @@ -159,6 +209,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
159 | 209 | writeValue(value: WidgetConfigComponentData): void { |
160 | 210 | this.modelValue = value; |
161 | 211 | if (this.modelValue) { |
212 | + if (this.widgetType !== this.modelValue.widgetType) { | |
213 | + this.widgetType = this.modelValue.widgetType; | |
214 | + this.buildForms(); | |
215 | + } | |
162 | 216 | const config = this.modelValue.config; |
163 | 217 | const layout = this.modelValue.layout; |
164 | 218 | if (config) { |
... | ... | @@ -184,23 +238,42 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
184 | 238 | }, null, 2); |
185 | 239 | this.units = config.units; |
186 | 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 | 241 | this.actions = config.actions; |
193 | 242 | if (!this.actions) { |
194 | 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 | 266 | if (this.isDataEnabled) { |
197 | 267 | if (this.widgetType !== widgetType.rpc && |
198 | 268 | this.widgetType !== widgetType.alarm && |
199 | 269 | this.widgetType !== widgetType.static) { |
270 | + const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray; | |
271 | + datasourcesFormArray.controls.length = 0; | |
200 | 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 | 278 | } else if (this.widgetType === widgetType.rpc) { |
206 | 279 | if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) { |
... | ... | @@ -215,10 +288,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
215 | 288 | this.targetDeviceAlias = null; |
216 | 289 | } |
217 | 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 | 299 | if (config.alarmSource) { |
223 | 300 | this.alarmSource = config.alarmSource; |
224 | 301 | } else { |
... | ... | @@ -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 | 336 | if (this.modelValue) { |
260 | 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 | 340 | this.propagateChange(this.modelValue); |
267 | 341 | } |
... | ... | @@ -271,12 +345,150 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
271 | 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 | 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 | 121 | export interface WidgetConfigComponentData { |
122 | 122 | config: WidgetConfig; |
123 | 123 | layout: WidgetLayout; |
124 | + widgetType: widgetType; | |
124 | 125 | } |
125 | 126 | |
126 | 127 | export const MissingWidgetType: WidgetInfo = { | ... | ... |
... | ... | @@ -18,7 +18,8 @@ |
18 | 18 | <div class="tb-dashboard-page mat-content" style="padding-top: 150px;" |
19 | 19 | fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen"> |
20 | 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 | 23 | <tb-dashboard-toolbar [fxShow]="!widgetEditMode" [forceFullscreen]="forceFullscreen" |
23 | 24 | [toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()"> |
24 | 25 | <div class="tb-dashboard-action-panels" fxLayout="column-reverse" fxLayout.gt-sm="row-reverse" |
... | ... | @@ -117,6 +118,7 @@ |
117 | 118 | <section class="tb-dashboard-container tb-absolute-fill" |
118 | 119 | [ngClass]="{ 'is-fullscreen': forceFullscreen, |
119 | 120 | 'tb-dashboard-toolbar-opened': toolbarOpened, |
121 | + 'tb-dashboard-toolbar-animated': isToolbarOpenedAnimate, | |
120 | 122 | 'tb-dashboard-toolbar-closed': !toolbarOpened }"> |
121 | 123 | <section *ngIf="!widgetEditMode" class="tb-dashboard-title" fxLayout="row" fxLayoutAlign="center center" |
122 | 124 | [ngStyle]="{'color': dashboard.configuration.settings.titleColor}"> | ... | ... |
... | ... | @@ -93,15 +93,18 @@ div.tb-dashboard-page { |
93 | 93 | @media #{$mat-gt-sm} { |
94 | 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 | 102 | &.tb-dashboard-toolbar-closed { |
102 | 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 | 110 | .tb-dashboard-layouts { | ... | ... |
... | ... | @@ -60,6 +60,17 @@ import { |
60 | 60 | import { WidgetComponentService } from '../../components/widget/widget-component.service'; |
61 | 61 | import { FormBuilder, FormGroup, NgForm } from '@angular/forms'; |
62 | 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 | 75 | @Component({ |
65 | 76 | selector: 'tb-dashboard-page', |
... | ... | @@ -89,6 +100,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
89 | 100 | isAddingWidget = false; |
90 | 101 | |
91 | 102 | isToolbarOpened = false; |
103 | + isToolbarOpenedAnimate = false; | |
92 | 104 | isRightLayoutOpened = false; |
93 | 105 | |
94 | 106 | editingWidget: Widget = null; |
... | ... | @@ -189,7 +201,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
189 | 201 | private widgetComponentService: WidgetComponentService, |
190 | 202 | private dashboardService: DashboardService, |
191 | 203 | private itembuffer: ItemBufferService, |
192 | - private fb: FormBuilder) { | |
204 | + private fb: FormBuilder, | |
205 | + private dialog: MatDialog) { | |
193 | 206 | super(store); |
194 | 207 | |
195 | 208 | this.editingWidgetFormGroup = this.fb.group({ |
... | ... | @@ -259,6 +272,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
259 | 272 | this.isAddingWidget = false; |
260 | 273 | |
261 | 274 | this.isToolbarOpened = false; |
275 | + this.isToolbarOpenedAnimate = false; | |
262 | 276 | this.isRightLayoutOpened = false; |
263 | 277 | |
264 | 278 | this.editingWidget = null; |
... | ... | @@ -283,10 +297,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
283 | 297 | } |
284 | 298 | |
285 | 299 | public openToolbar() { |
300 | + this.isToolbarOpenedAnimate = true; | |
286 | 301 | this.isToolbarOpened = true; |
287 | 302 | } |
288 | 303 | |
289 | 304 | public closeToolbar() { |
305 | + this.isToolbarOpenedAnimate = true; | |
290 | 306 | this.isToolbarOpened = false; |
291 | 307 | } |
292 | 308 | |
... | ... | @@ -420,8 +436,21 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
420 | 436 | if ($event) { |
421 | 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 | 456 | public openDashboardSettings($event: Event) { | ... | ... |
... | ... | @@ -28,7 +28,7 @@ import { IAliasController } from '@core/api/widget-api.models'; |
28 | 28 | import { Widget, WidgetActionSource, WidgetTypeParameters } from '@shared/models/widget.models'; |
29 | 29 | import { WidgetComponentService } from '@home/components/widget/widget-component.service'; |
30 | 30 | import { WidgetConfigComponentData } from '../../models/widget-component.models'; |
31 | -import { isString } from '@core/utils'; | |
31 | +import { isDefined, isString } from '@core/utils'; | |
32 | 32 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
33 | 33 | |
34 | 34 | @Component({ |
... | ... | @@ -92,13 +92,14 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan |
92 | 92 | const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget); |
93 | 93 | this.widgetConfig = { |
94 | 94 | config: this.widget.config, |
95 | - layout: this.widgetLayout | |
95 | + layout: this.widgetLayout, | |
96 | + widgetType: this.widget.type | |
96 | 97 | }; |
97 | 98 | const settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; |
98 | 99 | const dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; |
99 | 100 | this.typeParameters = widgetInfo.typeParameters; |
100 | 101 | this.actionSources = widgetInfo.actionSources; |
101 | - this.isDataEnabled = widgetInfo.typeParameters && !widgetInfo.typeParameters.useCustomDatasources; | |
102 | + this.isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true; | |
102 | 103 | if (!settingsSchema || settingsSchema === '') { |
103 | 104 | this.settingsSchema = {}; |
104 | 105 | } else { | ... | ... |
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 | 20 | Component, |
21 | 21 | ElementRef, |
22 | 22 | forwardRef, |
23 | - Input, | |
24 | - OnInit, | |
23 | + Input, OnChanges, | |
24 | + OnInit, SimpleChanges, | |
25 | 25 | SkipSelf, |
26 | 26 | ViewChild |
27 | 27 | } from '@angular/core'; |
... | ... | @@ -49,7 +49,7 @@ import { emptyPageData } from '@shared/models/page/page-data'; |
49 | 49 | @Component({ |
50 | 50 | selector: 'tb-entity-list', |
51 | 51 | templateUrl: './entity-list.component.html', |
52 | - styleUrls: [], | |
52 | + styleUrls: ['./entity-list.component.scss'], | |
53 | 53 | providers: [ |
54 | 54 | { |
55 | 55 | provide: NG_VALUE_ACCESSOR, |
... | ... | @@ -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 | 63 | entityListFormGroup: FormGroup; |
64 | 64 | |
65 | 65 | modelValue: Array<string> | null; |
66 | 66 | |
67 | - entityTypeValue: EntityType; | |
68 | - | |
69 | 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 | 70 | private requiredValue: boolean; |
78 | 71 | get required(): boolean { |
... | ... | @@ -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 | 150 | ngAfterViewInit(): void { |
147 | 151 | } |
148 | 152 | |
... | ... | @@ -159,7 +163,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV |
159 | 163 | this.searchText = ''; |
160 | 164 | if (value != null && value.length > 0) { |
161 | 165 | this.modelValue = [...value]; |
162 | - this.entityService.getEntities(this.entityTypeValue, value).subscribe( | |
166 | + this.entityService.getEntities(this.entityType, value).subscribe( | |
163 | 167 | (entities) => { |
164 | 168 | this.entities = entities; |
165 | 169 | this.entityListFormGroup.get('entities').setValue(this.entities); |
... | ... | @@ -218,7 +222,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV |
218 | 222 | |
219 | 223 | fetchEntities(searchText?: string): Observable<Array<BaseData<EntityId>>> { |
220 | 224 | this.searchText = searchText; |
221 | - return this.entityService.getEntitiesByNameFilter(this.entityTypeValue, searchText, | |
225 | + return this.entityService.getEntitiesByNameFilter(this.entityType, searchText, | |
222 | 226 | 50, '', false, true).pipe( |
223 | 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 | 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 | 19 | <mat-label *ngIf="showLabel">{{ 'entity.type' | translate }}</mat-label> |
20 | 20 | <mat-select [required]="required" |
21 | 21 | class="tb-entity-type-select" matInput formControlName="entityType"> | ... | ... |
... | ... | @@ -45,8 +45,14 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, |
45 | 45 | @Input() |
46 | 46 | useAliasEntityTypes: boolean; |
47 | 47 | |
48 | + private showLabelValue: boolean; | |
49 | + get showLabel(): boolean { | |
50 | + return this.showLabelValue; | |
51 | + } | |
48 | 52 | @Input() |
49 | - showLabel: boolean; | |
53 | + set showLabel(value: boolean) { | |
54 | + this.showLabelValue = coerceBooleanProperty(value); | |
55 | + } | |
50 | 56 | |
51 | 57 | private requiredValue: boolean; |
52 | 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 | 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 | 22 | export enum AliasFilterType { |
20 | 23 | singleEntity = 'singleEntity', |
... | ... | @@ -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 | 58 | entityList?: string[]; |
59 | +} | |
60 | + | |
61 | +export interface EntityNameFilter { | |
62 | + entityType?: EntityType; | |
54 | 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 | 135 | export interface EntityAlias { |
... | ... | @@ -62,9 +137,14 @@ export interface EntityAlias { |
62 | 137 | alias: string; |
63 | 138 | filter: EntityAliasFilter; |
64 | 139 | [key: string]: any; |
65 | - // TODO: | |
66 | 140 | } |
67 | 141 | |
68 | 142 | export interface EntityAliases { |
69 | 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 | 19 | import {TenantId} from '@shared/models/id/tenant-id'; |
20 | 20 | import {CustomerId} from '@shared/models/id/customer-id'; |
21 | 21 | import {DeviceCredentialsId} from '@shared/models/id/device-credentials-id'; |
22 | +import { EntitySearchQuery } from '@shared/models/relation.models'; | |
22 | 23 | |
23 | 24 | export interface Asset extends BaseData<AssetId> { |
24 | 25 | tenantId: TenantId; |
... | ... | @@ -32,3 +33,7 @@ export interface AssetInfo extends Asset { |
32 | 33 | customerTitle: string; |
33 | 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 | 19 | import {TenantId} from '@shared/models/id/tenant-id'; |
20 | 20 | import {CustomerId} from '@shared/models/id/customer-id'; |
21 | 21 | import {DeviceCredentialsId} from '@shared/models/id/device-credentials-id'; |
22 | +import { EntitySearchQuery } from '@shared/models/relation.models'; | |
22 | 23 | |
23 | 24 | export interface Device extends BaseData<DeviceId> { |
24 | 25 | tenantId: TenantId; |
... | ... | @@ -52,3 +53,7 @@ export interface DeviceCredentials extends BaseData<DeviceCredentialsId> { |
52 | 53 | credentialsId: string; |
53 | 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 | 20 | import {CustomerId} from '@shared/models/id/customer-id'; |
21 | 21 | import {EntityViewId} from '@shared/models/id/entity-view-id'; |
22 | 22 | import {EntityId} from '@shared/models/id/entity-id'; |
23 | +import { EntitySearchQuery } from '@shared/models/relation.models'; | |
23 | 24 | |
24 | 25 | export interface AttributesEntityView { |
25 | 26 | cs: Array<string>; |
... | ... | @@ -48,3 +49,7 @@ export interface EntityViewInfo extends EntityView { |
48 | 49 | customerTitle: string; |
49 | 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 | 62 | rootId: string; |
63 | 63 | rootType: EntityType; |
64 | 64 | direction: EntitySearchDirection; |
65 | - relationTypeGroup: RelationTypeGroup; | |
66 | - maxLevel: number; | |
65 | + relationTypeGroup?: RelationTypeGroup; | |
66 | + maxLevel?: number; | |
67 | 67 | } |
68 | 68 | |
69 | 69 | export interface EntityRelationsQuery { |
... | ... | @@ -71,6 +71,11 @@ export interface EntityRelationsQuery { |
71 | 71 | filters: Array<EntityTypeFilter>; |
72 | 72 | } |
73 | 73 | |
74 | +export interface EntitySearchQuery { | |
75 | + parameters: RelationsSearchParameters; | |
76 | + relationType: string; | |
77 | +} | |
78 | + | |
74 | 79 | export interface EntityRelation { |
75 | 80 | from: EntityId; |
76 | 81 | to: EntityId; | ... | ... |
... | ... | @@ -96,6 +96,9 @@ import { DashboardSelectPanelComponent } from '@shared/components/dashboard-sele |
96 | 96 | import { DashboardSelectComponent } from '@shared/components/dashboard-select.component'; |
97 | 97 | import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component'; |
98 | 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 | 103 | @NgModule({ |
101 | 104 | providers: [ |
... | ... | @@ -122,6 +125,7 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe'; |
122 | 125 | HelpComponent, |
123 | 126 | TbCheckboxComponent, |
124 | 127 | TbSnackBarComponent, |
128 | + TbErrorComponent, | |
125 | 129 | BreadcrumbComponent, |
126 | 130 | UserMenuComponent, |
127 | 131 | TimewindowComponent, |
... | ... | @@ -135,12 +139,14 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe'; |
135 | 139 | DashboardAutocompleteComponent, |
136 | 140 | EntitySubTypeAutocompleteComponent, |
137 | 141 | EntitySubTypeSelectComponent, |
142 | + EntitySubTypeListComponent, | |
138 | 143 | EntityAutocompleteComponent, |
139 | 144 | EntityListComponent, |
140 | 145 | EntityTypeSelectComponent, |
141 | 146 | EntitySelectComponent, |
142 | 147 | EntityKeysListComponent, |
143 | 148 | EntityListSelectComponent, |
149 | + EntityTypeListComponent, | |
144 | 150 | RelationTypeAutocompleteComponent, |
145 | 151 | SocialSharePanelComponent, |
146 | 152 | JsonObjectEditComponent, |
... | ... | @@ -207,6 +213,7 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe'; |
207 | 213 | TbAnchorComponent, |
208 | 214 | HelpComponent, |
209 | 215 | TbCheckboxComponent, |
216 | + TbErrorComponent, | |
210 | 217 | BreadcrumbComponent, |
211 | 218 | UserMenuComponent, |
212 | 219 | TimewindowComponent, |
... | ... | @@ -218,12 +225,14 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe'; |
218 | 225 | DashboardAutocompleteComponent, |
219 | 226 | EntitySubTypeAutocompleteComponent, |
220 | 227 | EntitySubTypeSelectComponent, |
228 | + EntitySubTypeListComponent, | |
221 | 229 | EntityAutocompleteComponent, |
222 | 230 | EntityListComponent, |
223 | 231 | EntityTypeSelectComponent, |
224 | 232 | EntitySelectComponent, |
225 | 233 | EntityKeysListComponent, |
226 | 234 | EntityListSelectComponent, |
235 | + EntityTypeListComponent, | |
227 | 236 | RelationTypeAutocompleteComponent, |
228 | 237 | SocialSharePanelComponent, |
229 | 238 | JsonObjectEditComponent, | ... | ... |
... | ... | @@ -246,6 +246,15 @@ div { |
246 | 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 | 258 | pre.tb-highlight { |
250 | 259 | display: block; |
251 | 260 | padding: 15px; |
... | ... | @@ -303,6 +312,15 @@ pre.tb-highlight { |
303 | 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 | 324 | .tb-fullscreen { |
307 | 325 | position: fixed !important; |
308 | 326 | top: 0; | ... | ... |