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