Browse Source

UI: Widget Config. Entity aliases dialog.

pull/2150/head
Igor Kulikov 7 years ago
parent
commit
d987c86456
  1. 18
      ui-ngx/package-lock.json
  2. 1
      ui-ngx/package.json
  3. 8
      ui-ngx/src/app/core/http/asset.service.ts
  4. 7
      ui-ngx/src/app/core/http/device.service.ts
  5. 8
      ui-ngx/src/app/core/http/entity-view.service.ts
  6. 353
      ui-ngx/src/app/core/http/entity.service.ts
  7. 11
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  8. 64
      ui-ngx/src/app/core/services/utils.service.ts
  9. 17
      ui-ngx/src/app/core/utils.ts
  10. 76
      ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html
  11. 28
      ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss
  12. 164
      ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts
  13. 111
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html
  14. 49
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.scss
  15. 264
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts
  16. 25
      ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.html
  17. 36
      ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.scss
  18. 230
      ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts
  19. 244
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html
  20. 38
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.scss
  21. 218
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts
  22. 23
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  23. 66
      ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html
  24. 67
      ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss
  25. 123
      ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts
  26. 113
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  27. 3
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
  28. 286
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  29. 1
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  30. 4
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html
  31. 9
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.scss
  32. 35
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts
  33. 1
      ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.html
  34. 7
      ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.ts
  35. 5
      ui-ngx/src/app/shared/components/entity/entity-list-select.component.scss
  36. 22
      ui-ngx/src/app/shared/components/entity/entity-list.component.scss
  37. 32
      ui-ngx/src/app/shared/components/entity/entity-list.component.ts
  38. 57
      ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html
  39. 22
      ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.scss
  40. 317
      ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts
  41. 54
      ui-ngx/src/app/shared/components/entity/entity-type-list.component.html
  42. 262
      ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts
  43. 2
      ui-ngx/src/app/shared/components/entity/entity-type-select.component.html
  44. 10
      ui-ngx/src/app/shared/components/entity/entity-type-select.component.scss
  45. 8
      ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts
  46. 67
      ui-ngx/src/app/shared/components/tb-error.component.ts
  47. 96
      ui-ngx/src/app/shared/models/alias.models.ts
  48. 5
      ui-ngx/src/app/shared/models/asset.models.ts
  49. 5
      ui-ngx/src/app/shared/models/device.models.ts
  50. 5
      ui-ngx/src/app/shared/models/entity-view.models.ts
  51. 9
      ui-ngx/src/app/shared/models/relation.models.ts
  52. 9
      ui-ngx/src/app/shared/shared.module.ts
  53. 18
      ui-ngx/src/styles.scss

18
ui-ngx/package-lock.json

@ -1622,7 +1622,6 @@
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": { "requires": {
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
@ -5308,9 +5307,9 @@
"dev": true "dev": true
}, },
"handlebars": { "handlebars": {
"version": "4.1.2", "version": "4.4.2",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.2.tgz",
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", "integrity": "sha512-cIv17+GhL8pHHnRJzGu2wwcthL5sb8uDKBHvZ2Dtu5s1YNt0ljbzKbamnc+gr69y7bzwQiBdr5+hOpRd5pnOdg==",
"dev": true, "dev": true,
"requires": { "requires": {
"neo-async": "^2.6.0", "neo-async": "^2.6.0",
@ -6532,6 +6531,14 @@
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
"dev": true "dev": true
}, },
"json-schema-defaults": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema-defaults/-/json-schema-defaults-0.4.0.tgz",
"integrity": "sha512-UsUrkDVNvHTneyeQOYHH9ZHb3+6OjwYfJ831SdO0yjtXtYZ7Jh8BKWsuJYUQW7qckP5JhHawsg4GI6A5fMaR/Q==",
"requires": {
"argparse": "^1.0.9"
}
},
"json-schema-traverse": { "json-schema-traverse": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@ -9989,8 +9996,7 @@
"sprintf-js": { "sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
"dev": true
}, },
"sshpk": { "sshpk": {
"version": "1.16.1", "version": "1.16.1",

1
ui-ngx/package.json

@ -46,6 +46,7 @@
"jquery": "^3.4.1", "jquery": "^3.4.1",
"jquery.terminal": "^2.8.0", "jquery.terminal": "^2.8.0",
"js-beautify": "^1.10.2", "js-beautify": "^1.10.2",
"json-schema-defaults": "^0.4.0",
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"messageformat": "^2.3.0", "messageformat": "^2.3.0",
"moment": "^2.24.0", "moment": "^2.24.0",

8
ui-ngx/src/app/core/http/asset.service.ts

@ -21,7 +21,8 @@ import {HttpClient} from '@angular/common/http';
import {PageLink} from '@shared/models/page/page-link'; import {PageLink} from '@shared/models/page/page-link';
import {PageData} from '@shared/models/page/page-data'; import {PageData} from '@shared/models/page/page-data';
import {EntitySubtype} from '@app/shared/models/entity-type.models'; import {EntitySubtype} from '@app/shared/models/entity-type.models';
import {Asset, AssetInfo} from '@app/shared/models/asset.models'; import {Asset, AssetInfo, AssetSearchQuery} from '@app/shared/models/asset.models';
import { Device, DeviceSearchQuery } from '@shared/models/device.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -81,4 +82,9 @@ export class AssetService {
return this.http.delete(`/api/customer/asset/${assetId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); return this.http.delete(`/api/customer/asset/${assetId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
} }
public findByQuery(query: AssetSearchQuery,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<Asset>> {
return this.http.post<Array<Asset>>('/api/assets', query, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
} }

7
ui-ngx/src/app/core/http/device.service.ts

@ -23,7 +23,7 @@ import { PageData } from '@shared/models/page/page-data';
import { Tenant } from '@shared/models/tenant.model'; import { Tenant } from '@shared/models/tenant.model';
import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models'; import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models';
import {map} from 'rxjs/operators'; import {map} from 'rxjs/operators';
import {DeviceInfo, Device, DeviceCredentials} from '@app/shared/models/device.models'; import { DeviceInfo, Device, DeviceCredentials, DeviceSearchQuery } from '@app/shared/models/device.models';
import {EntitySubtype} from '@app/shared/models/entity-type.models'; import {EntitySubtype} from '@app/shared/models/entity-type.models';
import {AuthService} from '../auth/auth.service'; import {AuthService} from '../auth/auth.service';
@ -115,4 +115,9 @@ export class DeviceService {
return this.http.delete(`/api/customer/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); return this.http.delete(`/api/customer/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
} }
public findByQuery(query: DeviceSearchQuery,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<Device>> {
return this.http.post<Array<Device>>('/api/devices', query, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
} }

8
ui-ngx/src/app/core/http/entity-view.service.ts

@ -21,7 +21,8 @@ import {HttpClient} from '@angular/common/http';
import {PageLink} from '@shared/models/page/page-link'; import {PageLink} from '@shared/models/page/page-link';
import {PageData} from '@shared/models/page/page-data'; import {PageData} from '@shared/models/page/page-data';
import {EntitySubtype} from '@app/shared/models/entity-type.models'; import {EntitySubtype} from '@app/shared/models/entity-type.models';
import {EntityView, EntityViewInfo} from '@app/shared/models/entity-view.models'; import { EntityView, EntityViewInfo, EntityViewSearchQuery } from '@app/shared/models/entity-view.models';
import { Asset, AssetSearchQuery } from '@shared/models/asset.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -80,4 +81,9 @@ export class EntityViewService {
return this.http.delete(`/api/customer/entityView/${entityViewId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); return this.http.delete(`/api/customer/entityView/${entityViewId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
} }
public findByQuery(query: EntityViewSearchQuery,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityView>> {
return this.http.post<Array<EntityView>>('/api/entityViews', query, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
} }

353
ui-ngx/src/app/core/http/entity.service.ts

@ -33,16 +33,29 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { Tenant } from '@shared/models/tenant.model'; import { Tenant } from '@shared/models/tenant.model';
import { catchError, concatMap, expand, map, toArray } from 'rxjs/operators'; import { catchError, concatMap, expand, map, mergeMap, toArray } from 'rxjs/operators';
import { Customer } from '@app/shared/models/customer.model'; import { Customer } from '@app/shared/models/customer.model';
import { AssetService } from '@core/http/asset.service'; import { AssetService } from '@core/http/asset.service';
import { EntityViewService } from '@core/http/entity-view.service'; import { EntityViewService } from '@core/http/entity-view.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { defaultHttpOptions } from '@core/http/http-utils'; import { defaultHttpOptions } from '@core/http/http-utils';
import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainService } from '@core/http/rule-chain.service';
import { SubscriptionInfo } from '@core/api/widget-api.models'; import { StateParams, SubscriptionInfo } from '@core/api/widget-api.models';
import { Datasource, DatasourceType, KeyInfo } from '@app/shared/models/widget.models'; import { Datasource, DatasourceType, KeyInfo } from '@app/shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { AliasFilterType, EntityAliasFilter, EntityAliasFilterResult } from '@shared/models/alias.models';
import { EntityInfo } from '@shared/models/entity.models';
import {
EntityRelationInfo,
EntityRelationsQuery,
EntitySearchDirection,
EntitySearchQuery
} from '@shared/models/relation.models';
import { EntityRelationService } from '@core/http/entity-relation.service';
import { isDefined } from '../utils';
import { AssetSearchQuery } from '@shared/models/asset.models';
import { DeviceSearchQuery } from '@shared/models/device.models';
import { EntityViewSearchQuery } from '@shared/models/entity-view.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -60,6 +73,7 @@ export class EntityService {
private userService: UserService, private userService: UserService,
private ruleChainService: RuleChainService, private ruleChainService: RuleChainService,
private dashboardService: DashboardService, private dashboardService: DashboardService,
private entityRelationService: EntityRelationService,
private utils: UtilsService private utils: UtilsService
) { } ) { }
@ -348,8 +362,62 @@ export class EntityService {
} }
} }
public getAliasFilterTypesByEntityTypes(entityTypes: Array<EntityType | AliasEntityType>): Array<AliasFilterType> {
const allAliasFilterTypes: Array<AliasFilterType> = Object.keys(AliasFilterType).map((key) => AliasFilterType[key]);
if (!entityTypes || !entityTypes.length) {
return allAliasFilterTypes;
}
const result = [];
for (const aliasFilterType of allAliasFilterTypes) {
if (this.filterAliasFilterTypeByEntityTypes(aliasFilterType, entityTypes)) {
result.push(aliasFilterType);
}
}
return result;
}
private filterAliasFilterTypeByEntityTypes(aliasFilterType: AliasFilterType,
entityTypes: Array<EntityType | AliasEntityType>): boolean {
if (!entityTypes || !entityTypes.length) {
return true;
}
let valid = false;
entityTypes.forEach((entityType) => {
valid = valid || this.filterAliasFilterTypeByEntityType(aliasFilterType, entityType);
});
return valid;
}
private filterAliasFilterTypeByEntityType(aliasFilterType: AliasFilterType, entityType: EntityType | AliasEntityType): boolean {
switch (aliasFilterType) {
case AliasFilterType.singleEntity:
return true;
case AliasFilterType.entityList:
return true;
case AliasFilterType.entityName:
return true;
case AliasFilterType.stateEntity:
return true;
case AliasFilterType.assetType:
return entityType === EntityType.ASSET;
case AliasFilterType.deviceType:
return entityType === EntityType.DEVICE;
case AliasFilterType.entityViewType:
return entityType === EntityType.ENTITY_VIEW;
case AliasFilterType.relationsQuery:
return true;
case AliasFilterType.assetSearchQuery:
return entityType === EntityType.ASSET;
case AliasFilterType.deviceSearchQuery:
return entityType === EntityType.DEVICE;
case AliasFilterType.entityViewSearchQuery:
return entityType === EntityType.ENTITY_VIEW;
}
return false;
}
public prepareAllowedEntityTypesList(allowedEntityTypes: Array<EntityType | AliasEntityType>, public prepareAllowedEntityTypesList(allowedEntityTypes: Array<EntityType | AliasEntityType>,
useAliasEntityTypes: boolean): Array<EntityType | AliasEntityType> { useAliasEntityTypes?: boolean): Array<EntityType | AliasEntityType> {
const authUser = getCurrentAuthUser(this.store); const authUser = getCurrentAuthUser(this.store);
const entityTypes: Array<EntityType | AliasEntityType> = []; const entityTypes: Array<EntityType | AliasEntityType> = [];
switch (authUser.authority) { switch (authUser.authority) {
@ -444,6 +512,285 @@ export class EntityService {
} }
} }
public resolveAliasFilter(filter: EntityAliasFilter, stateParams: StateParams,
maxItems: number, failOnEmpty: boolean): Observable<EntityAliasFilterResult> {
const result: EntityAliasFilterResult = {
entities: [],
stateEntity: false
};
if (filter.stateEntityParamName && filter.stateEntityParamName.length) {
result.entityParamName = filter.stateEntityParamName;
}
const stateEntityId = this.getStateEntityId(filter, stateParams);
switch (filter.type) {
case AliasFilterType.singleEntity:
const aliasEntityId = this.resolveAliasEntityId(filter.singleEntity.entityType, filter.singleEntity.id);
return this.getEntity(aliasEntityId.entityType as EntityType, aliasEntityId.id, true, true).pipe(
map((entity) => {
result.entities = this.entitiesToEntitiesInfo([entity]);
return result;
}
));
break;
case AliasFilterType.entityList:
return this.getEntities(filter.entityType, filter.entityList, true, true).pipe(
map((entities) => {
if (entities && entities.length || !failOnEmpty) {
result.entities = this.entitiesToEntitiesInfo(entities);
return result;
} else {
throw new Error();
}
}
));
break;
case AliasFilterType.entityName:
return this.getEntitiesByNameFilter(filter.entityType, filter.entityNameFilter, maxItems,
'', true, true).pipe(
map((entities) => {
if (entities && entities.length || !failOnEmpty) {
result.entities = this.entitiesToEntitiesInfo(entities);
return result;
} else {
throw new Error();
}
}
)
);
break;
case AliasFilterType.stateEntity:
result.stateEntity = true;
if (stateEntityId) {
return this.getEntity(stateEntityId.entityType as EntityType, stateEntityId.id, true, true).pipe(
map((entity) => {
result.entities = this.entitiesToEntitiesInfo([entity]);
return result;
}
));
} else {
return of(result);
}
break;
case AliasFilterType.assetType:
return this.getEntitiesByNameFilter(EntityType.ASSET, filter.assetNameFilter, maxItems,
filter.assetType, true, true).pipe(
map((entities) => {
if (entities && entities.length || !failOnEmpty) {
result.entities = this.entitiesToEntitiesInfo(entities);
return result;
} else {
throw new Error();
}
}
)
);
break;
case AliasFilterType.deviceType:
return this.getEntitiesByNameFilter(EntityType.DEVICE, filter.deviceNameFilter, maxItems,
filter.deviceType, true, true).pipe(
map((entities) => {
if (entities && entities.length || !failOnEmpty) {
result.entities = this.entitiesToEntitiesInfo(entities);
return result;
} else {
throw new Error();
}
}
)
);
break;
case AliasFilterType.entityViewType:
return this.getEntitiesByNameFilter(EntityType.ENTITY_VIEW, filter.entityViewNameFilter, maxItems,
filter.entityViewType, true, true).pipe(
map((entities) => {
if (entities && entities.length || !failOnEmpty) {
result.entities = this.entitiesToEntitiesInfo(entities);
return result;
} else {
throw new Error();
}
}
)
);
break;
case AliasFilterType.relationsQuery:
result.stateEntity = filter.rootStateEntity;
let rootEntityType;
let rootEntityId;
if (result.stateEntity && stateEntityId) {
rootEntityType = stateEntityId.entityType;
rootEntityId = stateEntityId.id;
} else if (!result.stateEntity) {
rootEntityType = filter.rootEntity.entityType;
rootEntityId = filter.rootEntity.id;
}
if (rootEntityType && rootEntityId) {
const relationQueryRootEntityId = this.resolveAliasEntityId(rootEntityType, rootEntityId);
const searchQuery: EntityRelationsQuery = {
parameters: {
rootId: relationQueryRootEntityId.id,
rootType: relationQueryRootEntityId.entityType as EntityType,
direction: filter.direction
},
filters: filter.filters
};
searchQuery.parameters.maxLevel = filter.maxLevel && filter.maxLevel > 0 ? filter.maxLevel : -1;
return this.entityRelationService.findInfoByQuery(searchQuery, true, true).pipe(
mergeMap((allRelations) => {
if (allRelations && allRelations.length || !failOnEmpty) {
if (isDefined(maxItems) && maxItems > 0 && allRelations) {
const limit = Math.min(allRelations.length, maxItems);
allRelations.length = limit;
}
return this.entityRelationInfosToEntitiesInfo(allRelations, filter.direction).pipe(
map((entities) => {
result.entities = entities;
return result;
})
);
} else {
return throwError(null);
}
})
);
} else {
return of(result);
}
break;
case AliasFilterType.assetSearchQuery:
case AliasFilterType.deviceSearchQuery:
case AliasFilterType.entityViewSearchQuery:
result.stateEntity = filter.rootStateEntity;
if (result.stateEntity && stateEntityId) {
rootEntityType = stateEntityId.entityType;
rootEntityId = stateEntityId.id;
} else if (!result.stateEntity) {
rootEntityType = filter.rootEntity.entityType;
rootEntityId = filter.rootEntity.id;
}
if (rootEntityType && rootEntityId) {
const searchQueryRootEntityId = this.resolveAliasEntityId(rootEntityType, rootEntityId);
const searchQuery: EntitySearchQuery = {
parameters: {
rootId: searchQueryRootEntityId.id,
rootType: searchQueryRootEntityId.entityType as EntityType,
direction: filter.direction
},
relationType: filter.relationType
};
let findByQueryObservable: Observable<Array<BaseData<EntityId>>>;
if (filter.type === AliasFilterType.assetSearchQuery) {
const assetSearchQuery = searchQuery as AssetSearchQuery;
assetSearchQuery.assetTypes = filter.assetTypes;
findByQueryObservable = this.assetService.findByQuery(assetSearchQuery, true, true);
} else if (filter.type === AliasFilterType.deviceSearchQuery) {
const deviceSearchQuery = searchQuery as DeviceSearchQuery;
deviceSearchQuery.deviceTypes = filter.deviceTypes;
findByQueryObservable = this.deviceService.findByQuery(deviceSearchQuery, true, true);
} else if (filter.type === AliasFilterType.entityViewSearchQuery) {
const entityViewSearchQuery = searchQuery as EntityViewSearchQuery;
entityViewSearchQuery.entityViewTypes = filter.entityViewTypes;
findByQueryObservable = this.entityViewService.findByQuery(entityViewSearchQuery, true, true);
}
return findByQueryObservable.pipe(
map((entities) => {
if (entities && entities.length || !failOnEmpty) {
if (isDefined(maxItems) && maxItems > 0 && entities) {
const limit = Math.min(entities.length, maxItems);
entities.length = limit;
}
result.entities = this.entitiesToEntitiesInfo(entities);
return result;
} else {
throw Error();
}
})
);
} else {
return of(result);
}
break;
}
}
private entitiesToEntitiesInfo(entities: Array<BaseData<EntityId>>): Array<EntityInfo> {
const entitiesInfo = [];
if (entities) {
entities.forEach((entity) => {
entitiesInfo.push(this.entityToEntityInfo(entity));
});
}
return entitiesInfo;
}
private entityToEntityInfo(entity: BaseData<EntityId>): EntityInfo {
return {
origEntity: entity,
name: entity.name,
label: (entity as any).label ? (entity as any).label : '',
entityType: entity.id.entityType as EntityType,
id: entity.id.id,
entityDescription: (entity as any).additionalInfo ? (entity as any).additionalInfo.description : ''
};
}
private entityRelationInfosToEntitiesInfo(entityRelations: Array<EntityRelationInfo>,
direction: EntitySearchDirection): Observable<Array<EntityInfo>> {
if (entityRelations) {
const tasks: Observable<EntityInfo>[] = [];
entityRelations.forEach((entityRelation) => {
tasks.push(this.entityRelationInfoToEntityInfo(entityRelation, direction));
});
return forkJoin(tasks);
} else {
return of([]);
}
}
private entityRelationInfoToEntityInfo(entityRelationInfo: EntityRelationInfo, direction: EntitySearchDirection): Observable<EntityInfo> {
const entityId = direction === EntitySearchDirection.FROM ? entityRelationInfo.to : entityRelationInfo.from;
return this.getEntity(entityId.entityType as EntityType, entityId.id, true, true).pipe(
map((entity) => {
return this.entityToEntityInfo(entity);
})
);
}
private getStateEntityId(filter: EntityAliasFilter, stateParams: StateParams): EntityId {
let entityId = null;
if (stateParams) {
if (filter.stateEntityParamName && filter.stateEntityParamName.length) {
if (stateParams[filter.stateEntityParamName]) {
entityId = stateParams[filter.stateEntityParamName].entityId;
}
} else {
entityId = stateParams.entityId;
}
}
if (!entityId) {
entityId = filter.defaultStateEntity;
}
if (entityId) {
entityId = this.resolveAliasEntityId(entityId.entityType, entityId.id);
}
return entityId;
}
private resolveAliasEntityId(entityType: EntityType | AliasEntityType, id: string): EntityId {
const entityId: EntityId = {
entityType,
id
};
if (entityType === AliasEntityType.CURRENT_CUSTOMER) {
const authUser = getCurrentAuthUser(this.store);
entityId.entityType = EntityType.CUSTOMER;
if (authUser.authority === Authority.CUSTOMER_USER) {
entityId.id = authUser.customerId;
}
}
return entityId;
}
private createDatasourcesFromSubscriptionInfo(subscriptionInfo: SubscriptionInfo): Observable<Array<Datasource>> { private createDatasourcesFromSubscriptionInfo(subscriptionInfo: SubscriptionInfo): Observable<Array<Datasource>> {
subscriptionInfo = this.validateSubscriptionInfo(subscriptionInfo); subscriptionInfo = this.validateSubscriptionInfo(subscriptionInfo);
if (subscriptionInfo.type === DatasourceType.entity) { if (subscriptionInfo.type === DatasourceType.entity) {

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

@ -278,6 +278,17 @@ export class DashboardUtilsService {
} }
} }
public getWidgetsArray(dashboard: Dashboard): Array<Widget> {
const widgetsArray: Array<Widget> = [];
const dashboardConfiguration = dashboard.configuration;
const widgets = dashboardConfiguration.widgets;
for (const widgetId of Object.keys(widgets)) {
const widget = widgets[widgetId];
widgetsArray.push(widget);
}
return widgetsArray;
}
private validateAndUpdateEntityAliases(configuration: DashboardConfiguration, private validateAndUpdateEntityAliases(configuration: DashboardConfiguration,
datasourcesByAliasId: {[aliasId: string]: Array<Datasource>}, datasourcesByAliasId: {[aliasId: string]: Array<Datasource>},
targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration { targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration {

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

@ -17,19 +17,39 @@
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { ExceptionData } from '@app/shared/models/error.models'; import { ExceptionData } from '@app/shared/models/error.models';
import { isUndefined, isDefined, deepClone } from '@core/utils'; import { deepClone, isDefined, isUndefined, deleteNullProperties } from '@core/utils';
import { WindowMessage } from '@shared/models/window-message.model'; import { WindowMessage } from '@shared/models/window-message.model';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { customTranslationsPrefix } from '@app/shared/models/constants'; import { customTranslationsPrefix } from '@app/shared/models/constants';
import { DataKey, Datasource, DatasourceType, KeyInfo, Widget } from '@shared/models/widget.models'; import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models';
import { alarmFields } from '@shared/models/alarm.models'; import { alarmFields } from '@shared/models/alarm.models';
import { materialColors } from '@app/shared/models/material.models'; import { materialColors } from '@app/shared/models/material.models';
import { WidgetInfo } from '@home/models/widget-component.models'; import { WidgetInfo } from '@home/models/widget-component.models';
import jsonSchemaDefaults from 'json-schema-defaults';
const varsRegex = /\$\{([^}]*)\}/g; const varsRegex = /\$\{([^}]*)\}/g;
const predefinedFunctions: {[func: string]: string} = {
Sin: 'return Math.round(1000*Math.sin(time/5000));',
Cos: 'return Math.round(1000*Math.cos(time/5000));',
Random: 'var value = prevValue + Math.random() * 100 - 50;\n' +
'var multiplier = Math.pow(10, 2 || 0);\n' +
'var value = Math.round(value * multiplier) / multiplier;\n' +
'if (value < -1000) {\n' +
' value = -1000;\n' +
'} else if (value > 1000) {\n' +
' value = 1000;\n' +
'}\n' +
'return value;'
};
const predefinedFunctionsList: Array<string> = [];
for (const func of Object.keys(predefinedFunctions)) {
predefinedFunctionsList.push(func);
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
@ -39,6 +59,22 @@ export class UtilsService {
widgetEditMode = false; widgetEditMode = false;
editWidgetInfo: WidgetInfo = null; editWidgetInfo: WidgetInfo = null;
defaultDataKey: DataKey = {
name: 'f(x)',
type: DataKeyType.function,
label: 'Sin',
color: this.getMaterialColor(0),
funcBody: this.getPredefinedFunctionBody('Sin'),
settings: {},
_hash: Math.random()
};
defaultDatasource: Datasource = {
type: DatasourceType.function,
name: DatasourceType.function,
dataKeys: [deepClone(this.defaultDataKey)]
};
constructor(@Inject(WINDOW) private window: Window, constructor(@Inject(WINDOW) private window: Window,
private translate: TranslateService) { private translate: TranslateService) {
let frame: Element = null; let frame: Element = null;
@ -57,6 +93,28 @@ export class UtilsService {
} }
} }
public getPredefinedFunctionsList(): Array<string> {
return predefinedFunctionsList;
}
public getPredefinedFunctionBody(func: string): string {
return predefinedFunctions[func];
}
public getDefaultDatasource(dataKeySchema: any): Datasource {
const datasource = deepClone(this.defaultDatasource);
if (isDefined(dataKeySchema)) {
datasource.dataKeys[0].settings = this.generateObjectFromJsonSchema(dataKeySchema);
}
return datasource;
}
public generateObjectFromJsonSchema(schema: any): any {
const obj = jsonSchemaDefaults(schema);
deleteNullProperties(obj);
return obj;
}
public hashCode(str: string): number { public hashCode(str: string): number {
let hash = 0; let hash = 0;
let i: number; let i: number;
@ -192,7 +250,7 @@ export class UtilsService {
return datasources; return datasources;
} }
public getMaterialColor(index) { public getMaterialColor(index: number) {
const colorIndex = index % materialColors.length; const colorIndex = index % materialColors.length;
return materialColors[colorIndex].value; return materialColors[colorIndex].value;
} }

17
ui-ngx/src/app/core/utils.ts

@ -103,6 +103,23 @@ export function isString(value: any): boolean {
return typeof value === 'string'; return typeof value === 'string';
} }
export function deleteNullProperties(obj: any) {
if (isUndefined(obj) || obj == null) {
return;
}
Object.keys(obj).forEach((propName) => {
if (obj[propName] === null || isUndefined(obj[propName])) {
delete obj[propName];
} else if (isObject(obj[propName])) {
deleteNullProperties(obj[propName]);
} else if (obj[propName] instanceof Array) {
(obj[propName] as any[]).forEach((elem) => {
deleteNullProperties(elem);
});
}
});
}
export function objToBase64(obj: any): string { export function objToBase64(obj: any): string {
const json = JSON.stringify(obj); const json = JSON.stringify(obj);
const encoded = utf8Encode(json); const encoded = utf8Encode(json);

76
ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html

@ -0,0 +1,76 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form #entityAliasForm="ngForm" [formGroup]="entityAliasFormGroup" (ngSubmit)="save()" style="width: 600px;">
<mat-toolbar fxLayout="row" color="primary">
<h2>{{ (isAdd ? 'alias.add' : 'alias.edit') | translate }}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<div fxLayout="row">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alias.name</mat-label>
<input matInput formControlName="alias" required>
<mat-error *ngIf="entityAliasFormGroup.get('alias').hasError('required')">
{{ 'alias.name-required' | translate }}
</mat-error>
<mat-error *ngIf="entityAliasFormGroup.get('alias').hasError('duplicateAliasName')">
{{ 'alias.duplicate-alias' | translate }}
</mat-error>
</mat-form-field>
<section class="tb-resolve-multiple-switch" fxLayout="column" fxLayoutAlign="start center">
<label class="tb-small resolve-multiple-label" translate>alias.resolve-multiple</label>
<mat-slide-toggle class="resolve-multiple-switch"
formControlName="resolveMultiple">
</mat-slide-toggle>
</section>
</div>
<tb-entity-filter formControlName="filter"
[resolveMultiple]="entityAliasFormGroup.get('resolveMultiple').value"
(resolveMultipleChanged)="entityAliasFormGroup.get('resolveMultiple').patchValue($event)"
[allowedEntityTypes]="allowedEntityTypes">
</tb-entity-filter>
<tb-error [error]="entityAliasFormGroup.hasError('noEntityMatched')
? translate.instant('alias.entity-filter-no-entity-matched') : ''"></tb-error>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || entityAliasFormGroup.invalid || !entityAliasFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.save') | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

28
ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-resolve-multiple-switch {
padding-left: 10px;
.resolve-multiple-switch {
margin: 0;
}
.resolve-multiple-label {
margin: 5px 0;
}
}
}

164
ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts

@ -0,0 +1,164 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
AbstractControl,
FormArray,
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NgForm, Validators, ValidatorFn
} from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { AttributeData } from '@shared/models/telemetry/telemetry.models';
import { EntityAlias, EntityAliases, EntityAliasFilter } from '@shared/models/alias.models';
import { DatasourceType, Widget, widgetType } from '@shared/models/widget.models';
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogService } from '@core/services/dialog.service';
import { EntityService } from '@core/http/entity.service';
import { Observable, of } from 'rxjs';
export interface EntityAliasDialogData {
isAdd: boolean;
allowedEntityTypes: Array<EntityType | AliasEntityType>;
entityAliases: EntityAliases | Array<EntityAlias>;
alias?: EntityAlias;
}
@Component({
selector: 'tb-entity-alias-dialog',
templateUrl: './entity-alias-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: EntityAliasDialogComponent}],
styleUrls: ['./entity-alias-dialog.component.scss']
})
export class EntityAliasDialogComponent extends DialogComponent<EntityAliasDialogComponent, EntityAlias>
implements OnInit, ErrorStateMatcher {
isAdd: boolean;
allowedEntityTypes: Array<EntityType | AliasEntityType>;
entityAliases: Array<EntityAlias>;
alias: EntityAlias;
entityAliasFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: EntityAliasDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<EntityAliasDialogComponent, EntityAlias>,
private fb: FormBuilder,
private utils: UtilsService,
private translate: TranslateService,
private entityService: EntityService) {
super(store, router, dialogRef);
this.isAdd = data.isAdd;
this.allowedEntityTypes = data.allowedEntityTypes;
if (Array.isArray(data.entityAliases)) {
this.entityAliases = data.entityAliases;
} else {
this.entityAliases = [];
for (const aliasId of Object.keys(data.entityAliases)) {
this.entityAliases.push(data.entityAliases[aliasId]);
}
}
if (this.isAdd) {
this.alias = {
id: null,
alias: '',
filter: {
resolveMultiple: false
}
};
} else {
this.alias = data.alias;
}
this.entityAliasFormGroup = this.fb.group({
alias: [this.alias.alias, [this.validateDuplicateAliasName(), Validators.required]],
resolveMultiple: [this.alias.filter.resolveMultiple],
filter: [this.alias.filter, Validators.required]
});
}
validateDuplicateAliasName(): ValidatorFn {
return (c: FormControl) => {
const newAlias = c.value;
const found = this.entityAliases.find((entityAlias) => entityAlias.alias === newAlias);
if (found) {
if (this.isAdd || this.alias.id !== found.id) {
return {
duplicateAliasName: {
valid: false
}
};
}
}
return null;
};
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
private validate(): Observable<any> {
return this.entityService.resolveAliasFilter(this.alias.filter, null, 1, true);
}
save(): void {
this.submitted = true;
this.alias.alias = this.entityAliasFormGroup.get('alias').value;
this.alias.filter = this.entityAliasFormGroup.get('filter').value;
this.alias.filter.resolveMultiple = this.entityAliasFormGroup.get('resolveMultiple').value;
this.validate().subscribe(() => {
if (this.isAdd) {
this.alias.id = this.utils.guid();
}
this.dialogRef.close(this.alias);
},
() => {
this.entityAliasFormGroup.setErrors({
noEntityMatched: true
});
const changesSubscriptuion = this.entityAliasFormGroup.valueChanges.subscribe(() => {
this.entityAliasFormGroup.setErrors(null);
changesSubscriptuion.unsubscribe();
});
}
);
}
}

111
ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html

@ -0,0 +1,111 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form #entityAliasesForm="ngForm" [formGroup]="entityAliasesFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar fxLayout="row" color="primary">
<h2>{{ title | translate }}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div class="tb-aliases-header" fxFlex fxLayout="row" fxLayoutAlign="start center">
<span fxFlex="5"></span>
<div fxFlex="95" fxLayout="row" fxLayoutAlign="start center">
<span class="tb-header-label" translate fxFlex="150px">alias.name</span>
<span class="tb-header-label" translate fxFlex style="padding-left: 10px;">alias.entity-filter</span>
<span class="tb-header-label" translate fxFlex="120px" style="padding-left: 10px;">alias.resolve-multiple</span>
<span style="min-width: 80px;"></span>
</div>
</div>
<mat-divider></mat-divider>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="row" fxLayoutAlign="start center"
formArrayName="entityAliases"
*ngFor="let entityAliasControl of entityAliasesFormGroup.get('entityAliases').controls; let $index = index">
<span fxFlex="5">{{$index + 1}}.</span>
<div class="mat-elevation-z4 tb-alias" fxFlex="95" fxLayout="row" fxLayoutAlign="start center">
<mat-form-field floatLabel="always" hideRequiredMarker class="mat-block" fxFlex="150px">
<mat-label></mat-label>
<input matInput [formControl]="entityAliasControl.get('alias')" required placeholder="{{ 'entity.alias' | translate }}">
<mat-error *ngIf="entityAliasControl.get('alias').hasError('required')">
{{ 'entity.alias-required' | translate }}
</mat-error>
</mat-form-field>
<tb-entity-filter-view fxFlex style="padding-left: 10px;" [formControl]="entityAliasControl.get('filter')">
</tb-entity-filter-view>
<section fxFlex="120px" style="padding-left: 10px;"
class="tb-resolve-multiple-switch"
fxLayout="column"
fxLayoutAlign="center center">
<mat-slide-toggle class="resolve-multiple-switch"
[formControl]="entityAliasControl.get('resolveMultiple')">
</mat-slide-toggle>
</section>
<button [disabled]="isLoading$ | async"
mat-button mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="editAlias($index)"
matTooltip="{{ 'alias.edit' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button [disabled]="isLoading$ | async"
mat-button mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="removeAlias($index)"
matTooltip="{{ 'entity.remove-alias' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<button mat-button mat-raised-button color="primary"
type="button"
(click)="addAlias()"
[fxShow]="!disableAdd"
[disabled]="isLoading$ | async"
matTooltip="{{ 'alias.add' | translate }}"
matTooltipPosition="above">
{{ 'alias.add' | translate }}
</button>
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || entityAliasesFormGroup.invalid || !entityAliasesFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

49
ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.scss

@ -0,0 +1,49 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-aliases-header {
min-height: 40px;
padding: 0 34px 0 34px;
margin: 5px;
.tb-header-label {
font-size: 14px;
color: rgba(0, 0, 0, .570588);
}
}
.tb-alias {
padding: 0 0 0 10px;
margin: 5px;
.tb-resolve-multiple-switch {
padding-left: 10px;
.resolve-multiple-switch {
margin: 0;
}
}
}
}
:host ::ng-deep {
.mat-dialog-content {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
}

264
ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts

@ -0,0 +1,264 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
AbstractControl,
FormArray,
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NgForm, Validators
} from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { AttributeData } from '@shared/models/telemetry/telemetry.models';
import { EntityAlias, EntityAliases, EntityAliasFilter } from '@shared/models/alias.models';
import { DatasourceType, Widget, widgetType } from '@shared/models/widget.models';
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogService } from '@core/services/dialog.service';
import { deepClone } from '@core/utils';
import { MatDialog } from '@angular/material/dialog';
import { EntityAliasDialogComponent, EntityAliasDialogData } from './entity-alias-dialog.component';
export interface EntityAliasesDialogData {
entityAliases: EntityAliases;
widgets: Array<Widget>;
isSingleEntityAlias: boolean;
isSingleWidget?: boolean;
allowedEntityTypes?: Array<AliasEntityType>;
disableAdd?: boolean;
singleEntityAlias?: EntityAlias;
customTitle?: string;
}
@Component({
selector: 'tb-entity-aliases-dialog',
templateUrl: './entity-aliases-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: EntityAliasesDialogComponent}],
styleUrls: ['./entity-aliases-dialog.component.scss']
})
export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesDialogComponent, EntityAliases>
implements OnInit, ErrorStateMatcher {
title: string;
disableAdd: boolean;
allowedEntityTypes: Array<EntityType | AliasEntityType>;
aliasToWidgetsMap: {[aliasId: string]: Array<string>} = {};
entityAliasesFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: EntityAliasesDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<EntityAliasesDialogComponent, EntityAliases>,
private fb: FormBuilder,
private utils: UtilsService,
private translate: TranslateService,
private dialogs: DialogService,
private dialog: MatDialog) {
super(store, router, dialogRef);
this.title = data.customTitle ? data.customTitle : 'entity.aliases';
this.disableAdd = this.data.disableAdd;
this.allowedEntityTypes = this.data.allowedEntityTypes;
if (data.widgets) {
let widgetsTitleList: Array<string>;
if (this.data.isSingleWidget && this.data.widgets.length === 1) {
const widget = this.data.widgets[0];
widgetsTitleList = [widget.config.title];
for (const aliasId of Object.keys(this.data.entityAliases)) {
this.aliasToWidgetsMap[aliasId] = widgetsTitleList;
}
} else {
this.data.widgets.forEach((widget) => {
if (widget.type === widgetType.rpc) {
if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
const targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
widgetsTitleList = this.aliasToWidgetsMap[targetDeviceAliasId];
if (!widgetsTitleList) {
widgetsTitleList = [];
this.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList;
}
widgetsTitleList.push(widget.config.title);
}
} else {
const datasources = this.utils.validateDatasources(widget.config.datasources);
datasources.forEach((datasource) => {
if (datasource.type === DatasourceType.entity && datasource.entityAliasId) {
widgetsTitleList = this.aliasToWidgetsMap[datasource.entityAliasId];
if (!widgetsTitleList) {
widgetsTitleList = [];
this.aliasToWidgetsMap[datasource.entityAliasId] = widgetsTitleList;
}
widgetsTitleList.push(widget.config.title);
}
});
}
});
}
}
const entityAliasControls: Array<AbstractControl> = [];
for (const aliasId of Object.keys(this.data.entityAliases)) {
const entityAlias = this.data.entityAliases[aliasId];
let filter = entityAlias.filter;
if (!filter) {
filter = {
resolveMultiple: false
};
}
if (!filter.resolveMultiple) {
filter.resolveMultiple = false;
}
entityAliasControls.push(this.createEntityAliasFormControl(aliasId, entityAlias));
}
this.entityAliasesFormGroup = this.fb.group({
entityAliases: this.fb.array(entityAliasControls)
});
}
private createEntityAliasFormControl(aliasId: string, entityAlias: EntityAlias): AbstractControl {
const aliasFormControl = this.fb.group({
id: [aliasId],
alias: [entityAlias ? entityAlias.alias : null, [Validators.required]],
filter: [entityAlias ? entityAlias.filter : null],
resolveMultiple: [entityAlias ? entityAlias.filter.resolveMultiple : false]
});
aliasFormControl.get('resolveMultiple').valueChanges.subscribe((resolveMultiple: boolean) => {
(aliasFormControl.get('filter').value as EntityAliasFilter).resolveMultiple = resolveMultiple;
});
return aliasFormControl;
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
removeAlias(index: number) {
const entityAlias = (this.entityAliasesFormGroup.get('entityAliases').value as any[])[index];
const widgetsTitleList = this.aliasToWidgetsMap[entityAlias.id];
if (widgetsTitleList) {
let widgetsListHtml = '';
for (const widgetTitle of widgetsTitleList) {
widgetsListHtml += '<br/>\'' + widgetTitle + '\'';
}
const message = this.translate.instant('entity.unable-delete-entity-alias-text',
{entityAlias: entityAlias.alias, widgetsList: widgetsListHtml});
this.dialogs.alert(this.translate.instant('entity.unable-delete-entity-alias-title'),
message, this.translate.instant('action.close'), true);
} else {
(this.entityAliasesFormGroup.get('entityAliases') as FormArray).removeAt(index);
}
}
public addAlias() {
this.openAliasDialog(-1);
}
public editAlias(index: number) {
this.openAliasDialog(index);
}
private openAliasDialog(index: number) {
const isAdd = index === -1;
let alias;
const aliasesArray = this.entityAliasesFormGroup.get('entityAliases').value as any[];
if (!isAdd) {
alias = aliasesArray[index];
}
this.dialog.open<EntityAliasDialogComponent, EntityAliasDialogData,
EntityAlias>(EntityAliasDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd,
allowedEntityTypes: this.allowedEntityTypes,
entityAliases: aliasesArray,
alias: isAdd ? null : deepClone(alias)
}
}).afterClosed().subscribe((entityAlias) => {
if (entityAlias) {
if (isAdd) {
(this.entityAliasesFormGroup.get('entityAliases') as FormArray)
.push(this.createEntityAliasFormControl(entityAlias.id, entityAlias));
} else {
const aliasFormControl = (this.entityAliasesFormGroup.get('entityAliases') as FormArray).at(index);
aliasFormControl.get('alias').patchValue(entityAlias.alias);
aliasFormControl.get('filter').patchValue(entityAlias.filter);
aliasFormControl.get('resolveMultiple').patchValue(entityAlias.filter.resolveMultiple);
}
}
});
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
const entityAliases: EntityAliases = {};
const uniqueAliasList: {[alias: string]: string} = {};
let valid = true;
let message: string;
const aliasesArray = this.entityAliasesFormGroup.get('entityAliases').value as any[];
for (const aliasValue of aliasesArray) {
const aliasId: string = aliasValue.id;
const alias: string = aliasValue.alias;
const filter: EntityAliasFilter = aliasValue.filter;
if (uniqueAliasList[alias]) {
valid = false;
message = this.translate.instant('entity.duplicate-alias-error', {alias});
break;
} else if (!filter || !filter.type) {
valid = false;
message = this.translate.instant('entity.missing-entity-filter-error', {alias});
break;
} else {
uniqueAliasList[alias] = alias;
entityAliases[aliasId] = {id: aliasId, alias, filter};
}
}
if (valid) {
this.dialogRef.close(entityAliases);
} else {
this.store.dispatch(new ActionNotificationShow(
{
message,
type: 'error'
}));
}
}
}

25
ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.html

@ -0,0 +1,25 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div fxLayout="column" class="tb-entity-filter-view">
<div *ngIf="!filter || !filter.type; else filterView" class="entity-filter-empty" translate>alias.no-entity-filter-specified</div>
<ng-template #filterView>
<div fxLayout="column">
<div class="entity-filter-value">{{ filterDisplayValue }}</div>
</div>
</ng-template>
</div>

36
ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.scss

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-entity-filter-view {
.entity-filter-empty {
font-size: 14px;
line-height: 16px;
color: rgba(221, 44, 0, .87);
}
.entity-filter-type {
font-size: 14px;
line-height: 16px;
color: rgba(0, 0, 0, .570588);
}
.entity-filter-value {
font-size: 14px;
line-height: 16px;
color: rgba(0, 0, 0, .570588);
}
}
}

230
ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts

@ -0,0 +1,230 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { AliasFilterType, EntityAliasFilter } from '@shared/models/alias.models';
import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-entity-filter-view',
templateUrl: './entity-filter-view.component.html',
styleUrls: ['./entity-filter-view.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EntityFilterViewComponent),
multi: true
}
]
})
export class EntityFilterViewComponent implements ControlValueAccessor {
constructor(private translate: TranslateService) {}
filterDisplayValue: string;
filter: EntityAliasFilter;
registerOnChange(fn: any): void {
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
}
writeValue(filter: EntityAliasFilter): void {
this.filter = filter;
if (this.filter && this.filter.type) {
let entityType: EntityType | AliasEntityType;
let prefix: string;
let allEntitiesText;
let anyRelationText;
let relationTypeText;
let rootEntityText;
let directionText;
switch (this.filter.type) {
case AliasFilterType.singleEntity:
entityType = this.filter.singleEntity.entityType;
this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).list,
{count: 1});
break;
case AliasFilterType.entityList:
entityType = this.filter.entityType;
const count = this.filter.entityList.length;
this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).list,
{count});
break;
case AliasFilterType.entityName:
entityType = this.filter.entityType;
prefix = this.filter.entityNameFilter;
this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).nameStartsWith,
{prefix});
break;
case AliasFilterType.stateEntity:
this.filterDisplayValue = this.translate.instant('alias.filter-type-state-entity-description');
break;
case AliasFilterType.assetType:
const assetType = this.filter.assetType;
prefix = this.filter.assetNameFilter;
if (prefix && prefix.length) {
this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-type-and-name-description',
{assetType, prefix});
} else {
this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-type-description',
{assetType});
}
break;
case AliasFilterType.deviceType:
const deviceType = this.filter.deviceType;
prefix = this.filter.deviceNameFilter;
if (prefix && prefix.length) {
this.filterDisplayValue = this.translate.instant('alias.filter-type-device-type-and-name-description',
{deviceType, prefix});
} else {
this.filterDisplayValue = this.translate.instant('alias.filter-type-device-type-description',
{deviceType});
}
break;
case AliasFilterType.entityViewType:
const entityViewType = this.filter.entityViewType;
prefix = this.filter.entityViewNameFilter;
if (prefix && prefix.length) {
this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-type-and-name-description',
{entityViewType, prefix});
} else {
this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-type-description',
{entityViewType});
}
break;
case AliasFilterType.relationsQuery:
allEntitiesText = this.translate.instant('alias.all-entities');
anyRelationText = this.translate.instant('alias.any-relation');
if (this.filter.rootStateEntity) {
rootEntityText = this.translate.instant('alias.state-entity');
} else {
rootEntityText = this.translate.instant(entityTypeTranslations.get(this.filter.rootEntity.entityType).type);
}
directionText = this.translate.instant('relation.direction-type.' + this.filter.direction);
const relationFilters = this.filter.filters;
if (relationFilters && relationFilters.length) {
const relationFiltersDisplayValues = [];
relationFilters.forEach((relationFilter) => {
let entitiesText;
if (relationFilter.entityTypes && relationFilter.entityTypes.length) {
const entitiesNamesList = [];
relationFilter.entityTypes.forEach((filterEntityType) => {
entitiesNamesList.push(
this.translate.instant(entityTypeTranslations.get(filterEntityType).typePlural)
);
});
entitiesText = entitiesNamesList.join(', ');
} else {
entitiesText = allEntitiesText;
}
if (relationFilter.relationType && relationFilter.relationType.length) {
relationTypeText = `'${relationFilter.relationType}'`;
} else {
relationTypeText = anyRelationText;
}
const relationFilterDisplayValue = this.translate.instant('alias.filter-type-relations-query-description',
{
entities: entitiesText,
relationType: relationTypeText,
direction: directionText,
rootEntity: rootEntityText
}
);
relationFiltersDisplayValues.push(relationFilterDisplayValue);
});
this.filterDisplayValue = relationFiltersDisplayValues.join(', ');
} else {
this.filterDisplayValue = this.translate.instant('alias.filter-type-relations-query-description',
{
entities: allEntitiesText,
relationType: anyRelationText,
direction: directionText,
rootEntity: rootEntityText
}
);
}
break;
case AliasFilterType.assetSearchQuery:
case AliasFilterType.deviceSearchQuery:
case AliasFilterType.entityViewSearchQuery:
allEntitiesText = this.translate.instant('alias.all-entities');
anyRelationText = this.translate.instant('alias.any-relation');
if (this.filter.rootStateEntity) {
rootEntityText = this.translate.instant('alias.state-entity');
} else {
rootEntityText = this.translate.instant(entityTypeTranslations.get(this.filter.rootEntity.entityType).type);
}
directionText = this.translate.instant('relation.direction-type.' + this.filter.direction);
if (this.filter.relationType && this.filter.relationType.length) {
relationTypeText = `'${filter.relationType}'`;
} else {
relationTypeText = anyRelationText;
}
const translationValues: any = {
relationType: relationTypeText,
direction: directionText,
rootEntity: rootEntityText
};
if (this.filter.type === AliasFilterType.assetSearchQuery) {
const assetTypesQuoted = [];
this.filter.assetTypes.forEach((filterAssetType) => {
assetTypesQuoted.push(`'${filterAssetType}'`);
});
const assetTypesText = assetTypesQuoted.join(', ');
translationValues.assetTypes = assetTypesText;
this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-search-query-description',
translationValues
);
} else if (this.filter.type === AliasFilterType.deviceSearchQuery) {
const deviceTypesQuoted = [];
this.filter.deviceTypes.forEach((filterDeviceType) => {
deviceTypesQuoted.push(`'${filterDeviceType}'`);
});
const deviceTypesText = deviceTypesQuoted.join(', ');
translationValues.deviceTypes = deviceTypesText;
this.filterDisplayValue = this.translate.instant('alias.filter-type-device-search-query-description',
translationValues
);
} else if (this.filter.type === AliasFilterType.entityViewSearchQuery) {
const entityViewTypesQuoted = [];
this.filter.entityViewTypes.forEach((filterEntityViewType) => {
entityViewTypesQuoted.push(`'${filterEntityViewType}'`);
});
const entityViewTypesText = entityViewTypesQuoted.join(', ');
translationValues.entityViewTypes = entityViewTypesText;
this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-search-query-description',
translationValues
);
}
break;
default:
this.filterDisplayValue = this.filter.type;
break;
}
} else {
this.filterDisplayValue = '';
}
}
}

244
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html

@ -0,0 +1,244 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div fxLayout="column" [formGroup]="entityFilterFormGroup" class="tb-entity-filter">
<mat-form-field class="mat-block">
<mat-label translate>alias.filter-type</mat-label>
<mat-select required matInput formControlName="type">
<mat-option *ngFor="let type of aliasFilterTypes" [value]="type">
{{aliasFilterTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-error *ngIf="entityFilterFormGroup.get('type').hasError('required')">
{{ 'alias.filter-type-required' | translate }}
</mat-error>
</mat-form-field>
<section fxLayout="column" [formGroup]="filterFormGroup" [ngSwitch]="entityFilterFormGroup.get('type').value">
<ng-template [ngSwitchCase]="aliasFilterType.singleEntity">
<tb-entity-select required
useAliasEntityTypes="true"
formControlName="singleEntity">
</tb-entity-select>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.entityList">
<tb-entity-type-select required
showLabel
[allowedEntityTypes]="allowedEntityTypes"
formControlName="entityType">
</tb-entity-type-select>
<tb-entity-list required
[entityType]="filterFormGroup.get('entityType').value"
formControlName="entityList">
</tb-entity-list>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.entityName">
<tb-entity-type-select required
showLabel
[allowedEntityTypes]="allowedEntityTypes"
formControlName="entityType">
</tb-entity-type-select>
<mat-form-field class="mat-block">
<mat-label translate>entity.name-starts-with</mat-label>
<input matInput formControlName="entityNameFilter" required>
<mat-error *ngIf="filterFormGroup.get('entityNameFilter').hasError('required')">
{{ 'entity.entity-name-filter-required' | translate }}
</mat-error>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.stateEntity">
<mat-form-field floatLabel="always" class="mat-block">
<mat-label translate>alias.state-entity-parameter-name</mat-label>
<input matInput formControlName="stateEntityParamName"
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}">
</mat-form-field>
<div fxLayout="column">
<label class="tb-small">{{ 'alias.default-state-entity' | translate }}</label>
<tb-entity-select fxFlex
useAliasEntityTypes="true"
formControlName="defaultStateEntity">
</tb-entity-select>
</div>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.assetType">
<tb-entity-subtype-autocomplete required
formControlName="assetType"
[entityType]="entityType.ASSET">
</tb-entity-subtype-autocomplete>
<mat-form-field class="mat-block">
<mat-label translate>asset.name-starts-with</mat-label>
<input matInput formControlName="assetNameFilter">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.deviceType">
<tb-entity-subtype-autocomplete required
formControlName="deviceType"
[entityType]="entityType.DEVICE">
</tb-entity-subtype-autocomplete>
<mat-form-field class="mat-block">
<mat-label translate>device.name-starts-with</mat-label>
<input matInput formControlName="deviceNameFilter">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.entityViewType">
<tb-entity-subtype-autocomplete required
formControlName="entityViewType"
[entityType]="entityType.ENTITY_VIEW">
</tb-entity-subtype-autocomplete>
<mat-form-field class="mat-block">
<mat-label translate>entity-view.name-starts-with</mat-label>
<input matInput formControlName="entityViewNameFilter">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.relationsQuery">
<section fxLayout="column" id="relationsQueryFilter">
<label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
<section class="tb-root-state-entity-switch" fxLayout="row" fxLayoutAlign="start center" style="padding-left: 0px;">
<mat-slide-toggle class="root-state-entity-switch"
formControlName="rootStateEntity">
</mat-slide-toggle>
<label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
</section>
<div fxFlex fxLayout="row" *ngIf="!filterFormGroup.get('rootStateEntity').value">
<tb-entity-select fxFlex
required
useAliasEntityTypes="true"
formControlName="rootEntity">
</tb-entity-select>
</div>
<div fxFlex fxLayout="row" *ngIf="filterFormGroup.get('rootStateEntity').value">
<mat-form-field floatLabel="always" class="mat-block" style="margin-top: 14px; padding-right: 8px;">
<mat-label translate>alias.state-entity-parameter-name</mat-label>
<input matInput formControlName="stateEntityParamName"
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}">
</mat-form-field>
<div fxFlex fxLayout="column">
<label class="tb-small">{{ 'alias.default-state-entity' | translate }}</label>
<tb-entity-select fxFlex
useAliasEntityTypes="true"
formControlName="defaultStateEntity">
</tb-entity-select>
</div>
</div>
<div fxFlex fxLayoutGap="8px" fxLayout="row">
<mat-form-field class="mat-block" style="min-width: 100px;">
<mat-label translate>relation.direction</mat-label>
<mat-select required matInput formControlName="direction">
<mat-option *ngFor="let type of directionTypes" [value]="type">
{{ directionTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex floatLabel="always" class="mat-block">
<mat-label translate>alias.max-relation-level</mat-label>
<input matInput
type="number"
min="1"
step="1"
placeholder="{{ 'alias.unlimited-level' | translate }}"
formControlName="maxLevel">
</mat-form-field>
</div>
<div class="mat-caption" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>relation.relation-filters</div>
<tb-relation-filters
[allowedEntityTypes]="allowedEntityTypes"
formControlName="filters">
</tb-relation-filters>
</section>
</ng-template>
<ng-template [ngSwitchCase]="entityFilterFormGroup.get('type').value === aliasFilterType.assetSearchQuery ||
entityFilterFormGroup.get('type').value === aliasFilterType.deviceSearchQuery ||
entityFilterFormGroup.get('type').value === aliasFilterType.entityViewSearchQuery ?
entityFilterFormGroup.get('type').value : ''">
<label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
<section class="tb-root-state-entity-switch" fxLayout="row" fxLayoutAlign="start center" style="padding-left: 0px;">
<mat-slide-toggle class="root-state-entity-switch"
formControlName="rootStateEntity">
</mat-slide-toggle>
<label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
</section>
<div fxFlex fxLayout="row" *ngIf="!filterFormGroup.get('rootStateEntity').value">
<tb-entity-select fxFlex
required
useAliasEntityTypes="true"
formControlName="rootEntity">
</tb-entity-select>
</div>
<div fxFlex fxLayout="row" *ngIf="filterFormGroup.get('rootStateEntity').value">
<mat-form-field floatLabel="always" class="mat-block" style="margin-top: 14px; padding-right: 8px;">
<mat-label translate>alias.state-entity-parameter-name</mat-label>
<input matInput formControlName="stateEntityParamName"
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}">
</mat-form-field>
<div fxFlex fxLayout="column">
<label class="tb-small">{{ 'alias.default-state-entity' | translate }}</label>
<tb-entity-select fxFlex
useAliasEntityTypes="true"
formControlName="defaultStateEntity">
</tb-entity-select>
</div>
</div>
<div fxFlex fxLayoutGap="8px" fxLayout="row">
<mat-form-field class="mat-block" style="min-width: 100px;">
<mat-label translate>relation.direction</mat-label>
<mat-select required matInput formControlName="direction">
<mat-option *ngFor="let type of directionTypes" [value]="type">
{{ directionTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex floatLabel="always" class="mat-block">
<mat-label translate>alias.max-relation-level</mat-label>
<input matInput
type="number"
min="1"
step="1"
placeholder="{{ 'alias.unlimited-level' | translate }}"
formControlName="maxLevel">
</mat-form-field>
</div>
<div class="mat-caption" style="color: rgba(0,0,0,0.57);" translate>relation.relation-type</div>
<tb-relation-type-autocomplete
fxFlex
formControlName="relationType">
</tb-relation-type-autocomplete>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.assetSearchQuery">
<div class="mat-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>asset.asset-types</div>
<tb-entity-subtype-list
required
[entityType]="entityType.ASSET"
formControlName="assetTypes">
</tb-entity-subtype-list>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.deviceSearchQuery">
<div class="mat-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>device.device-types</div>
<tb-entity-subtype-list
required
[entityType]="entityType.DEVICE"
formControlName="deviceTypes">
</tb-entity-subtype-list>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.entityViewSearchQuery">
<div class="mat-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>entity-view.entity-view-types</div>
<tb-entity-subtype-list
required
[entityType]="entityType.ENTITY_VIEW"
formControlName="entityViewTypes">
</tb-entity-subtype-list>
</ng-template>
</section>
</div>

38
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.scss

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-entity-filter {
#relationsQueryFilter {
padding-top: 20px;
tb-entity-select {
min-height: 92px;
}
}
.tb-root-state-entity-switch {
padding-left: 10px;
.root-state-entity-switch {
margin: 0;
}
.root-state-entity-label {
margin: 5px 0 5px 10px;
}
}
}
}

218
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts

@ -0,0 +1,218 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder, FormControl,
FormGroup,
NG_VALUE_ACCESSOR,
ValidatorFn,
Validators
} from '@angular/forms';
import { AliasFilterType, aliasFilterTypeTranslationMap, EntityAliasFilter } from '@shared/models/alias.models';
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
import { EntityService } from '@core/http/entity.service';
import { EntitySearchDirection, entitySearchDirectionTranslations } from '@shared/models/relation.models';
@Component({
selector: 'tb-entity-filter',
templateUrl: './entity-filter.component.html',
styleUrls: ['./entity-filter.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EntityFilterComponent),
multi: true
}
]
})
export class EntityFilterComponent implements ControlValueAccessor, OnInit {
@Input() disabled: boolean;
@Input() allowedEntityTypes: Array<EntityType | AliasEntityType>;
@Input() resolveMultiple: boolean;
@Output() resolveMultipleChanged: EventEmitter<boolean> = new EventEmitter<boolean>();
entityFilterFormGroup: FormGroup;
filterFormGroup: FormGroup;
aliasFilterTypes: Array<AliasFilterType>;
aliasFilterType = AliasFilterType;
aliasFilterTypeTranslations = aliasFilterTypeTranslationMap;
entityType = EntityType;
directionTypes = Object.keys(EntitySearchDirection);
directionTypeTranslations = entitySearchDirectionTranslations;
private propagateChange = null;
constructor(private translate: TranslateService,
private entityService: EntityService,
private fb: FormBuilder) {
}
ngOnInit(): void {
this.aliasFilterTypes = this.entityService.getAliasFilterTypesByEntityTypes(this.allowedEntityTypes);
this.entityFilterFormGroup = this.fb.group({
type: [null, [Validators.required]]
});
this.entityFilterFormGroup.get('type').valueChanges.subscribe((type: AliasFilterType) => {
this.filterTypeChanged(type);
});
this.entityFilterFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
this.filterFormGroup = this.fb.group({});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(filter: EntityAliasFilter): void {
if (!filter) {
filter = {
type: null,
resolveMultiple: this.resolveMultiple
};
}
this.entityFilterFormGroup.get('type').patchValue(filter.type, {emitEvent: false});
if (filter && filter.type) {
this.updateFilterFormGroup(filter.type, filter);
}
}
private updateFilterFormGroup(type: AliasFilterType, filter?: EntityAliasFilter) {
switch (type) {
case AliasFilterType.singleEntity:
this.filterFormGroup = this.fb.group({
singleEntity: [filter ? filter.singleEntity : null, [Validators.required]]
});
break;
case AliasFilterType.entityList:
this.filterFormGroup = this.fb.group({
entityType: [filter ? filter.entityType : null, [Validators.required]],
entityList: [filter ? filter.entityList : [], [Validators.required]],
});
break;
case AliasFilterType.entityName:
this.filterFormGroup = this.fb.group({
entityType: [filter ? filter.entityType : null, [Validators.required]],
entityNameFilter: [filter ? filter.entityNameFilter : '', [Validators.required]],
});
break;
case AliasFilterType.stateEntity:
this.filterFormGroup = this.fb.group({
stateEntityParamName: [filter ? filter.stateEntityParamName : null, []],
defaultStateEntity: [filter ? filter.defaultStateEntity : null, []],
});
break;
case AliasFilterType.assetType:
this.filterFormGroup = this.fb.group({
assetType: [filter ? filter.assetType : null, [Validators.required]],
assetNameFilter: [filter ? filter.assetNameFilter : '', []],
});
break;
case AliasFilterType.deviceType:
this.filterFormGroup = this.fb.group({
deviceType: [filter ? filter.deviceType : null, [Validators.required]],
deviceNameFilter: [filter ? filter.deviceNameFilter : '', []],
});
break;
case AliasFilterType.entityViewType:
this.filterFormGroup = this.fb.group({
entityViewType: [filter ? filter.entityViewType : null, [Validators.required]],
entityViewNameFilter: [filter ? filter.entityViewNameFilter : '', []],
});
break;
case AliasFilterType.relationsQuery:
case AliasFilterType.assetSearchQuery:
case AliasFilterType.deviceSearchQuery:
case AliasFilterType.entityViewSearchQuery:
this.filterFormGroup = this.fb.group({
rootStateEntity: [filter ? filter.rootStateEntity : false, []],
stateEntityParamName: [filter ? filter.stateEntityParamName : null, []],
defaultStateEntity: [filter ? filter.defaultStateEntity : null, []],
rootEntity: [filter ? filter.rootEntity : null, (filter && filter.rootStateEntity) ? [] : [Validators.required]],
direction: [filter ? filter.direction : EntitySearchDirection.FROM, [Validators.required]],
maxLevel: [filter ? filter.maxLevel : 1, []],
});
this.filterFormGroup.get('rootStateEntity').valueChanges.subscribe((rootStateEntity: boolean) => {
this.filterFormGroup.get('rootEntity').setValidators(rootStateEntity ? [] : [Validators.required]);
this.filterFormGroup.get('rootEntity').updateValueAndValidity();
});
if (type === AliasFilterType.relationsQuery) {
this.filterFormGroup.addControl('filters',
this.fb.control(filter ? filter.filters : [], []));
} else {
this.filterFormGroup.addControl('relationType',
this.fb.control(filter ? filter.relationType : null, []));
if (type === AliasFilterType.assetSearchQuery) {
this.filterFormGroup.addControl('assetTypes',
this.fb.control(filter ? filter.assetTypes : [], [Validators.required]));
} else if (type === AliasFilterType.deviceSearchQuery) {
this.filterFormGroup.addControl('deviceTypes',
this.fb.control(filter ? filter.deviceTypes : [], [Validators.required]));
} else if (type === AliasFilterType.entityViewSearchQuery) {
this.filterFormGroup.addControl('entityViewTypes',
this.fb.control(filter ? filter.entityViewTypes : [], [Validators.required]));
}
}
break;
}
this.filterFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
private filterTypeChanged(type: AliasFilterType) {
let resolveMultiple = true;
if (type === AliasFilterType.singleEntity || type === AliasFilterType.stateEntity) {
resolveMultiple = false;
}
if (this.resolveMultiple !== resolveMultiple) {
this.resolveMultipleChanged.emit(resolveMultiple);
}
this.updateFilterFormGroup(type);
}
private updateModel() {
let filter = null;
if (this.entityFilterFormGroup.valid && this.filterFormGroup.valid) {
filter = {
type: this.entityFilterFormGroup.get('type').value,
resolveMultiple: this.resolveMultiple
};
filter = {...filter, ...this.filterFormGroup.value};
}
this.propagateChange(filter);
}
}

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

@ -41,6 +41,11 @@ import { LegendComponent } from '@home/components/widget/legend.component';
import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component'; import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component';
import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component'; import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { EntityAliasesDialogComponent } from '@home/components/alias/entity-aliases-dialog.component';
import { EntityFilterViewComponent } from './entity/entity-filter-view.component';
import { EntityAliasDialogComponent } from './alias/entity-alias-dialog.component';
import { EntityFilterComponent } from './entity/entity-filter.component';
import { RelationFiltersComponent } from './relation/relation-filters.component';
@NgModule({ @NgModule({
entryComponents: [ entryComponents: [
@ -52,7 +57,9 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com
AlarmDetailsDialogComponent, AlarmDetailsDialogComponent,
AddAttributeDialogComponent, AddAttributeDialogComponent,
EditAttributeValuePanelComponent, EditAttributeValuePanelComponent,
AliasesEntitySelectPanelComponent AliasesEntitySelectPanelComponent,
EntityAliasesDialogComponent,
EntityAliasDialogComponent
], ],
declarations: declarations:
[ [
@ -67,6 +74,7 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com
EventTableComponent, EventTableComponent,
RelationTableComponent, RelationTableComponent,
RelationDialogComponent, RelationDialogComponent,
RelationFiltersComponent,
AlarmTableHeaderComponent, AlarmTableHeaderComponent,
AlarmTableComponent, AlarmTableComponent,
AlarmDetailsDialogComponent, AlarmDetailsDialogComponent,
@ -75,10 +83,14 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com
EditAttributeValuePanelComponent, EditAttributeValuePanelComponent,
AliasesEntitySelectPanelComponent, AliasesEntitySelectPanelComponent,
AliasesEntitySelectComponent, AliasesEntitySelectComponent,
EntityAliasesDialogComponent,
EntityAliasDialogComponent,
DashboardComponent, DashboardComponent,
WidgetComponent, WidgetComponent,
LegendComponent, LegendComponent,
WidgetConfigComponent WidgetConfigComponent,
EntityFilterViewComponent,
EntityFilterComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -93,14 +105,19 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com
AuditLogTableComponent, AuditLogTableComponent,
EventTableComponent, EventTableComponent,
RelationTableComponent, RelationTableComponent,
RelationFiltersComponent,
AlarmTableComponent, AlarmTableComponent,
AlarmDetailsDialogComponent, AlarmDetailsDialogComponent,
AttributeTableComponent, AttributeTableComponent,
AliasesEntitySelectComponent, AliasesEntitySelectComponent,
EntityAliasesDialogComponent,
EntityAliasDialogComponent,
DashboardComponent, DashboardComponent,
WidgetComponent, WidgetComponent,
LegendComponent, LegendComponent,
WidgetConfigComponent WidgetConfigComponent,
EntityFilterViewComponent,
EntityFilterComponent
], ],
providers: [ providers: [
WidgetComponentService WidgetComponentService

66
ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html

@ -0,0 +1,66 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-relation-filters" [formGroup]="relationFiltersFormGroup">
<div class="header" [fxShow]="relationFiltersFormGroup.get('relationFilters').length">
<div fxLayout="row" fxLayoutAlign="start center">
<span class="cell" style="width: 200px; min-width: 200px;" translate>relation.type</span>
<span class="cell" fxFlex translate>entity.entity-types</span>
<span class="cell" style="width: 40px; min-width: 40px;">&nbsp;</span>
</div>
</div>
<div class="body" [fxShow]="relationFiltersFormGroup.get('relationFilters').length">
<div class="row" fxFlex fxLayout="row"
fxLayoutAlign="start center" formArrayName="relationFilters"
*ngFor="let relationFilterControl of relationFiltersFormGroup.get('relationFilters').controls; let $index = index">
<div class="mat-elevation-z1" fxFlex fxLayout="row" fxLayoutAlign="start center">
<tb-relation-type-autocomplete
class="cell" style="width: 200px; min-width: 200px;"
[formControl]="relationFilterControl.get('relationType')">
</tb-relation-type-autocomplete>
<tb-entity-type-list class="cell" fxFlex
[allowedEntityTypes]="allowedEntityTypes"
[formControl]="relationFilterControl.get('entityTypes')">
</tb-entity-type-list>
<button mat-button mat-icon-button color="primary"
type="button"
style="width: 40px; min-width: 40px;"
(click)="removeFilter($index)"
[disabled]="isLoading$ | async"
matTooltip="{{ 'relation.remove-relation-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</div>
<div class="any-filter" [fxShow]="!relationFiltersFormGroup.get('relationFilters').length">
<span fxLayoutAlign="center center"
class="tb-prompt" translate>relation.any-relation</span>
</div>
<div>
<button mat-button mat-raised-button color="primary"
[disabled]="isLoading$ | async"
(click)="addFilter()"
type="button"
matTooltip="{{ 'relation.add-relation-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
{{ 'action.add' | translate }}
</button>
</div>
</div>

67
ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss

@ -0,0 +1,67 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-relation-filters {
.header {
padding-right: 5px;
padding-bottom: 5px;
padding-left: 5px;
.cell {
padding-right: 5px;
padding-left: 5px;
font-size: 12px;
font-weight: 700;
color: rgba(0, 0, 0, .54);
white-space: nowrap;
}
}
.body {
max-height: 300px;
padding-right: 5px;
padding-bottom: 20px;
padding-left: 5px;
overflow: auto;
.row {
padding-top: 5px;
}
.cell {
padding-right: 5px;
padding-left: 5px;
}
}
}
}
:host ::ng-deep {
.tb-relation-filters {
.cell {
.mat-form-field {
.mat-form-field-infix {
border-top: none;
}
&.mat-form-field-appearance-standard {
.mat-form-field-flex {
padding-top: 0;
}
}
}
}
}
}

123
ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts

@ -0,0 +1,123 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor, FormArray,
FormBuilder, FormControl,
FormGroup,
NG_VALUE_ACCESSOR,
ValidatorFn,
Validators
} from '@angular/forms';
import { AliasFilterType, aliasFilterTypeTranslationMap, EntityAliasFilter } from '@shared/models/alias.models';
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
import { EntityService } from '@core/http/entity.service';
import { EntitySearchDirection, entitySearchDirectionTranslations, EntityTypeFilter } from '@shared/models/relation.models';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Subscription } from 'rxjs';
@Component({
selector: 'tb-relation-filters',
templateUrl: './relation-filters.component.html',
styleUrls: ['./relation-filters.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RelationFiltersComponent),
multi: true
}
]
})
export class RelationFiltersComponent extends PageComponent implements ControlValueAccessor, OnInit {
@Input() disabled: boolean;
@Input() allowedEntityTypes: Array<EntityType | AliasEntityType>;
relationFiltersFormGroup: FormGroup;
private propagateChange = null;
private valueChangeSubscription: Subscription = null;
constructor(protected store: Store<AppState>,
private fb: FormBuilder) {
super(store);
}
ngOnInit(): void {
this.relationFiltersFormGroup = this.fb.group({});
this.relationFiltersFormGroup.addControl('relationFilters',
this.fb.array([]));
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(filters: Array<EntityTypeFilter>): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const relationFiltersControls: Array<AbstractControl> = [];
if (filters && filters.length) {
filters.forEach((filter) => {
relationFiltersControls.push(this.createRelationFilterFormGroup(filter));
});
}
this.relationFiltersFormGroup.setControl('relationFilters', this.fb.array(relationFiltersControls));
this.valueChangeSubscription = this.relationFiltersFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
public removeFilter(index: number) {
(this.relationFiltersFormGroup.get('relationFilters') as FormArray).removeAt(index);
}
public addFilter() {
const relationFiltersFormArray = this.relationFiltersFormGroup.get('relationFilters') as FormArray;
const filter: EntityTypeFilter = {
relationType: null,
entityTypes: []
};
relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter));
}
private createRelationFilterFormGroup(filter: EntityTypeFilter): AbstractControl {
return this.fb.group({
relationType: [filter ? filter.relationType : null],
entityTypes: [filter ? filter.entityTypes : []]
});
}
private updateModel() {
const filters: Array<EntityTypeFilter> = this.relationFiltersFormGroup.get('relationFilters').value;
this.propagateChange(filters);
}
}

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

@ -16,26 +16,121 @@
--> -->
<mat-tab-group [ngClass]="{'tb-headless': (widgetType === widgetTypes.static && !displayAdvanced())}" <mat-tab-group [ngClass]="{'tb-headless': (widgetType === widgetTypes.static && !displayAdvanced())}"
fxFlex class="tb-widget-config tb-absolute-fill" [selectedIndex]="selectedTab"> fxFlex class="tb-widget-config tb-absolute-fill" [(selectedIndex)]="selectedTab">
<mat-tab label="{{ 'widget-config.data' | translate }}" [fxShow]="widgetType !== widgetTypes.static"> <mat-tab label="{{ 'widget-config.data' | translate }}" [fxShow]="widgetType !== widgetTypes.static">
<div class="mat-content mat-padding" fxLayout="column"> <div [formGroup]="dataSettings" class="mat-content mat-padding" fxLayout="column" fxLayoutGap="8px">
<div [fxShow]="widgetType === widgetTypes.timeseries || widgetType === widgetTypes.alarm" <div *ngIf="widgetType === widgetTypes.timeseries || widgetType === widgetTypes.alarm"
fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center"> fxLayout="column" fxLayoutGap="8px" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center">
<div fxLayout="column" fxFlex> <div fxLayout="column" fxLayoutGap="8px" fxFlex>
<mat-checkbox fxFlex [(ngModel)]="useDashboardTimewindow" (ngModelChange)="updateModel()"> <mat-checkbox fxFlex formControlName="useDashboardTimewindow">
{{ 'widget-config.use-dashboard-timewindow' | translate }} {{ 'widget-config.use-dashboard-timewindow' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox [disabled]="useDashboardTimewindow" fxFlex [(ngModel)]="displayTimewindow" (ngModelChange)="updateModel()"> <mat-checkbox fxFlex
formControlName="displayTimewindow">
{{ 'widget-config.display-timewindow' | translate }} {{ 'widget-config.display-timewindow' | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
<section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;"> <section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;">
<span [ngClass]="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span> <span [ngClass]="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span>
<tb-timewindow [disabled]="useDashboardTimewindow" asButton="true" <tb-timewindow asButton="true"
aggregation="{{ widgetType === widgetTypes.timeseries }}" aggregation="{{ widgetType === widgetTypes.timeseries }}"
fxFlex [(ngModel)]="timewindow" (ngModelChange)="updateModel()"></tb-timewindow> fxFlex formControlName="timewindow"></tb-timewindow>
</section> </section>
</div> </div>
<div *ngIf="widgetType === widgetTypes.alarm" fxLayout="column" fxLayoutGap="8px" fxLayoutAlign="center"
fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.alarm-status</mat-label>
<mat-select matInput formControlName="alarmSearchStatus">
<mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus">
{{ ('alarm.search-status.' + searchStatus) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.polling-interval</mat-label>
<input matInput required
formControlName="alarmsPollingInterval"
type="number"
step="1"/>
<mat-error *ngIf="dataSettings.get('alarmsPollingInterval').hasError('required')">
{{ 'alarm.polling-interval-required' | translate }}
</mat-error>
<mat-error *ngIf="dataSettings.get('alarmsPollingInterval').hasError('min')">
{{ 'alarm.min-polling-interval-message' | translate }}
</mat-error>
</mat-form-field>
</div>
<mat-expansion-panel class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc &&
widgetType !== widgetTypes.alarm &&
widgetType !== widgetTypes.static &&
isDataEnabled" [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title fxLayout="column">
<div class="tb-panel-title" translate>widget-config.datasources</div>
<div *ngIf="typeParameters && typeParameters.maxDatasources > -1"
class="tb-hint">{{ 'widget-config.maximum-datasources' | translate:{count: typeParameters.maxDatasources} }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="dataSettings.get('datasources').length === 0; else datasourcesTemplate">
<span translate fxLayoutAlign="center center"
class="tb-prompt">datasource.add-datasource-prompt</span>
</div>
<ng-template #datasourcesTemplate>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<span fxFlex="5"></span>
<div fxFlex fxLayout="row" fxLayoutAlign="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<span translate style="min-width: 110px;">widget-config.datasource-type</span>
<span fxHide fxShow.gt-sm translate fxFlex
style="padding-left: 10px;">widget-config.datasource-parameters</span>
<span style="min-width: 40px;"></span>
</div>
</div>
<div style="overflow: auto; padding-bottom: 15px;">
<div fxFlex fxLayout="row" fxLayoutAlign="start center"
formArrayName="datasources"
*ngFor="let datasourceControl of dataSettings.get('datasources').controls; let $index = index">
<span fxFlex="5">{{$index + 1}}.</span>
<div class="mat-elevation-z4" fxFlex fxLayout="row" fxLayoutAlign="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<div fxFlex>TODO: datasource: {{ datasourceControl.value | json }}</div>
<button [disabled]="isLoading$ | async"
mat-button mat-icon-button color="primary"
style="min-width: 40px;"
(click)="removeDatasource($index)"
matTooltip="{{ 'widget-config.remove-datasource' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</div>
</ng-template>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<button [disabled]="isLoading$ | async"
mat-button mat-raised-button color="primary"
[fxShow]="typeParameters &&
(typeParameters.maxDatasources == -1 || dataSettings.get('datasources').controls.length < typeParameters.maxDatasources)"
(click)="addDatasource()"
matTooltip="{{ 'widget-config.add-datasource' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
<span translate>action.add</span>
</button>
</div>
</mat-expansion-panel>
<mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.rpc &&
isDataEnabled" [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
{{ 'widget-config.target-device' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<div style="padding: 0 5px;">
</div>
</mat-expansion-panel>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'widget-config.settings' | translate }}"> <mat-tab label="{{ 'widget-config.settings' | translate }}">

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

@ -28,5 +28,8 @@
height: 100%; height: 100%;
} }
} }
.mat-expansion-panel.tb-datasources.mat-expanded {
overflow: visible;
}
} }
} }

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

@ -19,20 +19,35 @@ import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { import {
DataKey,
Datasource, Datasource,
DatasourceType,
LegendConfig, LegendConfig,
WidgetActionDescriptor, WidgetActionDescriptor,
WidgetActionSource, WidgetConfigSettings, WidgetActionSource,
WidgetConfigSettings,
widgetType, widgetType,
WidgetTypeParameters WidgetTypeParameters
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; import {
ControlValueAccessor,
FormArray,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator,
Validators
} from '@angular/forms';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { deepClone, isDefined } from '@app/core/utils'; import { deepClone, isDefined, isObject } from '@app/core/utils';
import { Timewindow } from '@shared/models/time/time.models'; import { alarmFields, AlarmSearchStatus } from '@shared/models/alarm.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { EntityAlias } from '@shared/models/alias.models'; import { EntityAlias } from '@shared/models/alias.models';
import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'tb-widget-config', selector: 'tb-widget-config',
@ -55,15 +70,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
widgetTypes = widgetType; widgetTypes = widgetType;
alarmSearchStatuses = Object.keys(AlarmSearchStatus);
@Input() @Input()
forceExpandDatasources: boolean; forceExpandDatasources: boolean;
@Input() @Input()
isDataEnabled: boolean; isDataEnabled: boolean;
@Input()
widgetType: widgetType;
@Input() @Input()
typeParameters: WidgetTypeParameters; typeParameters: WidgetTypeParameters;
@ -84,6 +98,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
@Input() disabled: boolean; @Input() disabled: boolean;
widgetType: widgetType;
selectedTab: number; selectedTab: number;
title: string; title: string;
showTitleIcon: boolean; showTitleIcon: boolean;
@ -101,17 +117,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
titleStyle: string; titleStyle: string;
units: string; units: string;
decimals: number; decimals: number;
useDashboardTimewindow: boolean;
displayTimewindow: boolean;
timewindow: Timewindow;
showLegend: boolean; showLegend: boolean;
legendConfig: LegendConfig; legendConfig: LegendConfig;
actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
datasources: Array<Datasource>;
targetDeviceAlias: EntityAlias; targetDeviceAlias: EntityAlias;
alarmSource: Datasource; alarmSource: Datasource;
alarmSearchStatus: AlarmSearchStatus;
alarmsPollingInterval: number;
settings: WidgetConfigSettings; settings: WidgetConfigSettings;
mobileOrder: number; mobileOrder: number;
mobileHeight: number; mobileHeight: number;
@ -138,11 +148,51 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
private propagateChange = null; private propagateChange = null;
constructor(protected store: Store<AppState>) { public dataSettings: FormGroup;
constructor(protected store: Store<AppState>,
private utils: UtilsService,
private translate: TranslateService,
private fb: FormBuilder) {
super(store); super(store);
} }
ngOnInit(): void { ngOnInit(): void {
}
private buildForms() {
this.dataSettings = this.fb.group({});
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null));
this.dataSettings.addControl('displayTimewindow', this.fb.control(null));
this.dataSettings.addControl('timewindow', this.fb.control(null));
this.dataSettings.get('useDashboardTimewindow').valueChanges.subscribe((value: boolean) => {
if (value) {
this.dataSettings.get('displayTimewindow').disable({emitEvent: false});
this.dataSettings.get('timewindow').disable({emitEvent: false});
} else {
this.dataSettings.get('displayTimewindow').enable({emitEvent: false});
this.dataSettings.get('timewindow').enable({emitEvent: false});
}
});
if (this.widgetType === widgetType.alarm) {
this.dataSettings.addControl('alarmSearchStatus', this.fb.control(null));
this.dataSettings.addControl('alarmsPollingInterval', this.fb.control(null,
[Validators.required, Validators.min(1)]));
}
}
if (this.isDataEnabled) {
if (this.widgetType !== widgetType.rpc &&
this.widgetType !== widgetType.alarm &&
this.widgetType !== widgetType.static) {
this.dataSettings.addControl('datasources',
this.fb.array([]));
}
}
this.dataSettings.valueChanges.subscribe(
() => this.updateModel()
);
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
@ -159,6 +209,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
writeValue(value: WidgetConfigComponentData): void { writeValue(value: WidgetConfigComponentData): void {
this.modelValue = value; this.modelValue = value;
if (this.modelValue) { if (this.modelValue) {
if (this.widgetType !== this.modelValue.widgetType) {
this.widgetType = this.modelValue.widgetType;
this.buildForms();
}
const config = this.modelValue.config; const config = this.modelValue.config;
const layout = this.modelValue.layout; const layout = this.modelValue.layout;
if (config) { if (config) {
@ -184,23 +238,42 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}, null, 2); }, null, 2);
this.units = config.units; this.units = config.units;
this.decimals = config.decimals; this.decimals = config.decimals;
this.useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ?
config.useDashboardTimewindow : true;
this.displayTimewindow = isDefined(config.displayTimewindow) ?
config.displayTimewindow : true;
this.timewindow = config.timewindow;
this.actions = config.actions; this.actions = config.actions;
if (!this.actions) { if (!this.actions) {
this.actions = {}; this.actions = {};
} }
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ?
config.useDashboardTimewindow : true;
this.dataSettings.patchValue(
{ useDashboardTimewindow }, {emitEvent: false}
);
if (useDashboardTimewindow) {
this.dataSettings.get('displayTimewindow').disable({emitEvent: false});
this.dataSettings.get('timewindow').disable({emitEvent: false});
} else {
this.dataSettings.get('displayTimewindow').enable({emitEvent: false});
this.dataSettings.get('timewindow').enable({emitEvent: false});
}
this.dataSettings.patchValue(
{ displayTimewindow: isDefined(config.displayTimewindow) ?
config.displayTimewindow : true }, {emitEvent: false}
);
this.dataSettings.patchValue(
{ timewindow: config.timewindow }, {emitEvent: false}
);
}
if (this.isDataEnabled) { if (this.isDataEnabled) {
if (this.widgetType !== widgetType.rpc && if (this.widgetType !== widgetType.rpc &&
this.widgetType !== widgetType.alarm && this.widgetType !== widgetType.alarm &&
this.widgetType !== widgetType.static) { this.widgetType !== widgetType.static) {
const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray;
datasourcesFormArray.controls.length = 0;
if (config.datasources) { if (config.datasources) {
this.datasources = config.datasources; config.datasources.forEach((datasource) => {
} else { datasourcesFormArray.controls.push(this.fb.control(datasource));
this.datasources = []; });
datasourcesFormArray.setValue(config.datasources, {emitEvent: false});
} }
} else if (this.widgetType === widgetType.rpc) { } else if (this.widgetType === widgetType.rpc) {
if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) { if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) {
@ -215,10 +288,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.targetDeviceAlias = null; this.targetDeviceAlias = null;
} }
} else if (this.widgetType === widgetType.alarm) { } else if (this.widgetType === widgetType.alarm) {
this.alarmSearchStatus = isDefined(config.alarmSearchStatus) ? this.dataSettings.patchValue(
config.alarmSearchStatus : AlarmSearchStatus.ANY; { alarmSearchStatus: isDefined(config.alarmSearchStatus) ?
this.alarmsPollingInterval = isDefined(config.alarmsPollingInterval) ? config.alarmSearchStatus : AlarmSearchStatus.ANY }, {emitEvent: false}
config.alarmsPollingInterval : 5; );
this.dataSettings.patchValue(
{ alarmsPollingInterval: isDefined(config.alarmsPollingInterval) ?
config.alarmsPollingInterval : 5}, {emitEvent: false}
);
if (config.alarmSource) { if (config.alarmSource) {
this.alarmSource = config.alarmSource; this.alarmSource = config.alarmSource;
} else { } else {
@ -255,13 +332,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} }
} }
public updateModel() { private updateModel() {
if (this.modelValue) { if (this.modelValue) {
if (this.modelValue.config) { if (this.modelValue.config) {
const config = this.modelValue.config; Object.assign(this.modelValue.config, this.dataSettings.value);
config.useDashboardTimewindow = this.useDashboardTimewindow;
config.displayTimewindow = this.displayTimewindow;
config.timewindow = this.timewindow;
} }
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
} }
@ -271,12 +345,150 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
return this.widgetSettingsSchema && this.widgetSettingsSchema.schema; return this.widgetSettingsSchema && this.widgetSettingsSchema.schema;
} }
public removeDatasource(index: number) {
(this.dataSettings.get('datasources') as FormArray).removeAt(index);
}
public addDatasource() {
let newDatasource: Datasource;
if (this.functionsOnly) {
newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema));
newDatasource.dataKeys = [this.generateDataKey('Sin', DataKeyType.function)];
} else {
newDatasource = { type: DatasourceType.entity,
dataKeys: []
};
}
const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray;
datasourcesFormArray.push(this.fb.control(newDatasource));
}
public generateDataKey(chip: any, type: DataKeyType): DataKey {
if (isObject(chip)) {
(chip as DataKey)._hash = Math.random();
return chip;
} else {
let label: string = chip;
if (type === DataKeyType.alarm) {
const alarmField = alarmFields[label];
if (alarmField) {
label = this.translate.instant(alarmField.name);
}
}
label = this.genNextLabel(label);
const result: DataKey = {
name: chip,
type,
label,
color: this.genNextColor(),
settings: {},
_hash: Math.random()
};
if (type === DataKeyType.function) {
result.name = 'f(x)';
result.funcBody = this.utils.getPredefinedFunctionBody(chip);
if (!result.funcBody) {
result.funcBody = 'return prevValue + 1;';
}
}
if (isDefined(this.dataKeySettingsSchema.schema)) {
result.settings = this.utils.generateObjectFromJsonSchema(this.dataKeySettingsSchema.schema);
}
return result;
}
}
private genNextLabel(name: string): string {
let label = name;
let i = 1;
let matches = false;
const datasources = this.widgetType === widgetType.alarm ? [this.modelValue.config.alarmSource] : this.modelValue.config.datasources;
if (datasources) {
do {
matches = false;
datasources.forEach((datasource) => {
if (datasource && datasource.dataKeys) {
datasource.dataKeys.forEach((dataKey) => {
if (dataKey.label === label) {
i++;
label = name + ' ' + i;
matches = true;
}
});
}
});
} while (matches);
}
return label;
}
private genNextColor(): string {
let i = 0;
const datasources = this.widgetType === widgetType.alarm ? [this.modelValue.config.alarmSource] : this.modelValue.config.datasources;
if (datasources) {
datasources.forEach((datasource) => {
if (datasource && datasource.dataKeys) {
i += datasource.dataKeys.length;
}
});
}
return this.utils.getMaterialColor(i);
}
public validate(c: FormControl) { public validate(c: FormControl) {
return null; /*{ if (!this.dataSettings.valid) {
targetDeviceAliasIds: { return {
valid: false, dataSettings: {
}, valid: false
};*/ }
};
} else {
const config = this.modelValue.config;
if (this.widgetType === widgetType.rpc && this.isDataEnabled) {
if (!config.targetDeviceAliasIds || !config.targetDeviceAliasIds.length) {
return {
targetDeviceAliasIds: {
valid: false
}
};
}
} else if (this.widgetType === widgetType.alarm && this.isDataEnabled) {
if (!config.alarmSource) {
return {
alarmSource: {
valid: false
}
};
}
} else if (this.widgetType !== widgetType.static && this.isDataEnabled) {
if (!config.datasources || !config.datasources.length) {
return {
datasources: {
valid: false
}
};
}
}
try {
JSON.parse(this.widgetStyle);
} catch (e) {
return {
widgetStyle: {
valid: false
}
};
}
try {
JSON.parse(this.titleStyle);
} catch (e) {
return {
titleStyle: {
valid: false
}
};
}
}
return null;
} }
} }

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

@ -121,6 +121,7 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
export interface WidgetConfigComponentData { export interface WidgetConfigComponentData {
config: WidgetConfig; config: WidgetConfig;
layout: WidgetLayout; layout: WidgetLayout;
widgetType: widgetType;
} }
export const MissingWidgetType: WidgetInfo = { export const MissingWidgetType: WidgetInfo = {

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

@ -18,7 +18,8 @@
<div class="tb-dashboard-page mat-content" style="padding-top: 150px;" <div class="tb-dashboard-page mat-content" style="padding-top: 150px;"
fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen"> fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
<section class="tb-dashboard-toolbar" <section class="tb-dashboard-toolbar"
[ngClass]="{ 'tb-dashboard-toolbar-opened': toolbarOpened, 'tb-dashboard-toolbar-closed': !toolbarOpened }"> [ngClass]="{ 'tb-dashboard-toolbar-opened': toolbarOpened,
'tb-dashboard-toolbar-closed': !toolbarOpened }">
<tb-dashboard-toolbar [fxShow]="!widgetEditMode" [forceFullscreen]="forceFullscreen" <tb-dashboard-toolbar [fxShow]="!widgetEditMode" [forceFullscreen]="forceFullscreen"
[toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()"> [toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()">
<div class="tb-dashboard-action-panels" fxLayout="column-reverse" fxLayout.gt-sm="row-reverse" <div class="tb-dashboard-action-panels" fxLayout="column-reverse" fxLayout.gt-sm="row-reverse"
@ -117,6 +118,7 @@
<section class="tb-dashboard-container tb-absolute-fill" <section class="tb-dashboard-container tb-absolute-fill"
[ngClass]="{ 'is-fullscreen': forceFullscreen, [ngClass]="{ 'is-fullscreen': forceFullscreen,
'tb-dashboard-toolbar-opened': toolbarOpened, 'tb-dashboard-toolbar-opened': toolbarOpened,
'tb-dashboard-toolbar-animated': isToolbarOpenedAnimate,
'tb-dashboard-toolbar-closed': !toolbarOpened }"> 'tb-dashboard-toolbar-closed': !toolbarOpened }">
<section *ngIf="!widgetEditMode" class="tb-dashboard-title" fxLayout="row" fxLayoutAlign="center center" <section *ngIf="!widgetEditMode" class="tb-dashboard-title" fxLayout="row" fxLayoutAlign="center center"
[ngStyle]="{'color': dashboard.configuration.settings.titleColor}"> [ngStyle]="{'color': dashboard.configuration.settings.titleColor}">

9
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.scss

@ -93,15 +93,18 @@ div.tb-dashboard-page {
@media #{$mat-gt-sm} { @media #{$mat-gt-sm} {
margin-top: $toolbar-height; margin-top: $toolbar-height;
} }
&.tb-dashboard-toolbar-animated {
transition: margin-top .3s cubic-bezier(.55, 0, .55, .2); transition: margin-top .3s cubic-bezier(.55, 0, .55, .2);
}
} }
} }
&.tb-dashboard-toolbar-closed { &.tb-dashboard-toolbar-closed {
margin-top: 0; margin-top: 0;
transition: margin-top .3s cubic-bezier(.55, 0, .55, .2) .2s; &.tb-dashboard-toolbar-animated {
transition: margin-top .3s cubic-bezier(.55, 0, .55, .2) .2s;
}
} }
.tb-dashboard-layouts { .tb-dashboard-layouts {

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

@ -60,6 +60,17 @@ import {
import { WidgetComponentService } from '../../components/widget/widget-component.service'; import { WidgetComponentService } from '../../components/widget/widget-component.service';
import { FormBuilder, FormGroup, NgForm } from '@angular/forms'; import { FormBuilder, FormGroup, NgForm } from '@angular/forms';
import { ItemBufferService } from '@core/services/item-buffer.service'; import { ItemBufferService } from '@core/services/item-buffer.service';
import {
DeviceCredentialsDialogComponent,
DeviceCredentialsDialogData
} from '@home/pages/device/device-credentials-dialog.component';
import { DeviceCredentials } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import {
EntityAliasesDialogComponent,
EntityAliasesDialogData
} from '@home/components/alias/entity-aliases-dialog.component';
import { EntityAliases } from '@app/shared/models/alias.models';
@Component({ @Component({
selector: 'tb-dashboard-page', selector: 'tb-dashboard-page',
@ -89,6 +100,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
isAddingWidget = false; isAddingWidget = false;
isToolbarOpened = false; isToolbarOpened = false;
isToolbarOpenedAnimate = false;
isRightLayoutOpened = false; isRightLayoutOpened = false;
editingWidget: Widget = null; editingWidget: Widget = null;
@ -189,7 +201,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private widgetComponentService: WidgetComponentService, private widgetComponentService: WidgetComponentService,
private dashboardService: DashboardService, private dashboardService: DashboardService,
private itembuffer: ItemBufferService, private itembuffer: ItemBufferService,
private fb: FormBuilder) { private fb: FormBuilder,
private dialog: MatDialog) {
super(store); super(store);
this.editingWidgetFormGroup = this.fb.group({ this.editingWidgetFormGroup = this.fb.group({
@ -259,6 +272,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.isAddingWidget = false; this.isAddingWidget = false;
this.isToolbarOpened = false; this.isToolbarOpened = false;
this.isToolbarOpenedAnimate = false;
this.isRightLayoutOpened = false; this.isRightLayoutOpened = false;
this.editingWidget = null; this.editingWidget = null;
@ -283,10 +297,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
public openToolbar() { public openToolbar() {
this.isToolbarOpenedAnimate = true;
this.isToolbarOpened = true; this.isToolbarOpened = true;
} }
public closeToolbar() { public closeToolbar() {
this.isToolbarOpenedAnimate = true;
this.isToolbarOpened = false; this.isToolbarOpened = false;
} }
@ -420,8 +436,21 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
// TODO: this.dialog.open<EntityAliasesDialogComponent, EntityAliasesDialogData,
this.dialogService.todo(); EntityAliases>(EntityAliasesDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
entityAliases: deepClone(this.dashboard.configuration.entityAliases),
widgets: this.dashboardUtils.getWidgetsArray(this.dashboard),
isSingleEntityAlias: false
}
}).afterClosed().subscribe((entityAliases) => {
if (entityAliases) {
this.dashboard.configuration.entityAliases = entityAliases;
this.entityAliasesUpdated();
}
});
} }
public openDashboardSettings($event: Event) { public openDashboardSettings($event: Event) {

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

@ -18,7 +18,6 @@
<form [formGroup]="widgetFormGroup"> <form [formGroup]="widgetFormGroup">
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
<tb-widget-config <tb-widget-config
[widgetType]="widget.type"
[typeParameters]="typeParameters" [typeParameters]="typeParameters"
[actionSources]="actionSources" [actionSources]="actionSources"
[isDataEnabled]="isDataEnabled" [isDataEnabled]="isDataEnabled"

7
ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.ts

@ -28,7 +28,7 @@ import { IAliasController } from '@core/api/widget-api.models';
import { Widget, WidgetActionSource, WidgetTypeParameters } from '@shared/models/widget.models'; import { Widget, WidgetActionSource, WidgetTypeParameters } from '@shared/models/widget.models';
import { WidgetComponentService } from '@home/components/widget/widget-component.service'; import { WidgetComponentService } from '@home/components/widget/widget-component.service';
import { WidgetConfigComponentData } from '../../models/widget-component.models'; import { WidgetConfigComponentData } from '../../models/widget-component.models';
import { isString } from '@core/utils'; import { isDefined, isString } from '@core/utils';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({ @Component({
@ -92,13 +92,14 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget); const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget);
this.widgetConfig = { this.widgetConfig = {
config: this.widget.config, config: this.widget.config,
layout: this.widgetLayout layout: this.widgetLayout,
widgetType: this.widget.type
}; };
const settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; const settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
const dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; const dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
this.typeParameters = widgetInfo.typeParameters; this.typeParameters = widgetInfo.typeParameters;
this.actionSources = widgetInfo.actionSources; this.actionSources = widgetInfo.actionSources;
this.isDataEnabled = widgetInfo.typeParameters && !widgetInfo.typeParameters.useCustomDatasources; this.isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
if (!settingsSchema || settingsSchema === '') { if (!settingsSchema || settingsSchema === '') {
this.settingsSchema = {}; this.settingsSchema = {};
} else { } else {

5
ui-ngx/src/app/shared/components/entity/entity-list-select.component.scss

@ -23,10 +23,5 @@
padding-top: 0; padding-top: 0;
} }
} }
.mat-form-field-flex {
.mat-form-field-infix {
border-top: 0;
}
}
} }
} }

22
ui-ngx/src/app/shared/components/entity/entity-list.component.scss

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
.mat-form-field {
.mat-form-field-infix {
border-top: none;
}
}
}

32
ui-ngx/src/app/shared/components/entity/entity-list.component.ts

@ -20,8 +20,8 @@ import {
Component, Component,
ElementRef, ElementRef,
forwardRef, forwardRef,
Input, Input, OnChanges,
OnInit, OnInit, SimpleChanges,
SkipSelf, SkipSelf,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
@ -49,7 +49,7 @@ import { emptyPageData } from '@shared/models/page/page-data';
@Component({ @Component({
selector: 'tb-entity-list', selector: 'tb-entity-list',
templateUrl: './entity-list.component.html', templateUrl: './entity-list.component.html',
styleUrls: [], styleUrls: ['./entity-list.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -58,21 +58,14 @@ import { emptyPageData } from '@shared/models/page/page-data';
} }
] ]
}) })
export class EntityListComponent implements ControlValueAccessor, OnInit, AfterViewInit { export class EntityListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges {
entityListFormGroup: FormGroup; entityListFormGroup: FormGroup;
modelValue: Array<string> | null; modelValue: Array<string> | null;
entityTypeValue: EntityType;
@Input() @Input()
set entityType(entityType: EntityType) { entityType: EntityType;
if (this.entityTypeValue !== entityType) {
this.entityTypeValue = entityType;
this.reset();
}
}
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {
@ -143,6 +136,17 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
); );
} }
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'entityType') {
this.reset();
}
}
}
}
ngAfterViewInit(): void { ngAfterViewInit(): void {
} }
@ -159,7 +163,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
this.searchText = ''; this.searchText = '';
if (value != null && value.length > 0) { if (value != null && value.length > 0) {
this.modelValue = [...value]; this.modelValue = [...value];
this.entityService.getEntities(this.entityTypeValue, value).subscribe( this.entityService.getEntities(this.entityType, value).subscribe(
(entities) => { (entities) => {
this.entities = entities; this.entities = entities;
this.entityListFormGroup.get('entities').setValue(this.entities); this.entityListFormGroup.get('entities').setValue(this.entities);
@ -218,7 +222,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
fetchEntities(searchText?: string): Observable<Array<BaseData<EntityId>>> { fetchEntities(searchText?: string): Observable<Array<BaseData<EntityId>>> {
this.searchText = searchText; this.searchText = searchText;
return this.entityService.getEntitiesByNameFilter(this.entityTypeValue, searchText, return this.entityService.getEntitiesByNameFilter(this.entityType, searchText,
50, '', false, true).pipe( 50, '', false, true).pipe(
map((data) => data ? data : [])); map((data) => data ? data : []));
} }

57
ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html

@ -0,0 +1,57 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field appearance="standard" [formGroup]="entitySubtypeListFormGroup" class="mat-block">
<mat-chip-list #chipList formControlName="entitySubtypeList">
<mat-chip
*ngFor="let entitySubtype of entitySubtypeList"
[selectable]="!disabled"
[removable]="!disabled"
(removed)="remove(entitySubtype)">
{{entitySubtype}}
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
</mat-chip>
<input matInput type="text" placeholder="{{ !disabled ? ((!entitySubtypeList || !entitySubtypeList.length) ? placeholder : secondaryPlaceholder) : '' }}"
style="max-width: 200px;"
#entitySubtypeInput
(focusin)="onFocus()"
formControlName="entitySubtype"
matAutocompleteOrigin
#origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin"
[matAutocomplete]="entitySubtypeAutocomplete"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="chipAdd($event)">
</mat-chip-list>
<mat-autocomplete #entitySubtypeAutocomplete="matAutocomplete"
class="tb-autocomplete"
(optionSelected)="selected($event)"
[displayWith]="displayEntitySubtypeFn">
<mat-option *ngFor="let entitySubtype of filteredEntitySubtypeList | async" [value]="entitySubtype">
<span [innerHTML]="entitySubtype | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredEntitySubtypeList | async)?.length" [value]="null">
<span>
{{ translate.get(noSubtypesMathingText, {entitySubtype: searchText}) | async }}
</span>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="entitySubtypeListFormGroup.get('entitySubtypeList').hasError('required')">
{{ subtypeListEmptyText | translate }}
</mat-error>
</mat-form-field>

22
ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.scss

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
.mat-form-field {
.mat-form-field-infix {
border-top: none;
}
}
}

317
ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts

@ -0,0 +1,317 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterContentInit,
AfterViewInit,
Component,
ElementRef,
forwardRef,
Input, OnDestroy,
OnInit,
SkipSelf,
ViewChild
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NG_VALUE_ACCESSOR, NgForm, Validators
} from '@angular/forms';
import { Observable, of, Subscription, throwError } from 'rxjs';
import { map, mergeMap, startWith, tap, share, pairwise, filter, publishReplay, refCount } from 'rxjs/operators';
import {Store} from '@ngrx/store';
import {AppState} from '@app/core/core.state';
import {TranslateService} from '@ngx-translate/core';
import { AliasEntityType, EntitySubtype, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import {BaseData} from '@shared/models/base-data';
import {EntityId} from '@shared/models/id/entity-id';
import {EntityService} from '@core/http/entity.service';
import {ErrorStateMatcher, MatAutocomplete, MatAutocompleteSelectedEvent, MatChipList} from '@angular/material';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { emptyPageData } from '@shared/models/page/page-data';
import { AssetService } from '@core/http/asset.service';
import { DeviceService } from '@core/http/device.service';
import { EntityViewService } from '@core/http/entity-view.service';
import { BroadcastService } from '@core/services/broadcast.service';
import { MatChipInputEvent } from '@angular/material/chips';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
@Component({
selector: 'tb-entity-subtype-list',
templateUrl: './entity-subtype-list.component.html',
styleUrls: ['./entity-subtype-list.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EntitySubTypeListComponent),
multi: true
}
]
})
export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy {
entitySubtypeListFormGroup: FormGroup;
modelValue: Array<string> | null;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
const newVal = coerceBooleanProperty(value);
if (this.requiredValue !== newVal) {
this.requiredValue = newVal;
this.updateValidators();
}
}
@Input()
disabled: boolean;
@Input()
entityType: EntityType;
@ViewChild('entitySubtypeInput', {static: false}) entitySubtypeInput: ElementRef<HTMLInputElement>;
@ViewChild('entitySubtypeAutocomplete', {static: false}) entitySubtypeAutocomplete: MatAutocomplete;
@ViewChild('chipList', {static: true}) chipList: MatChipList;
entitySubtypeList: Array<string> = [];
filteredEntitySubtypeList: Observable<Array<string>>;
entitySubtypes: Observable<Array<string>>;
private broadcastSubscription: Subscription;
placeholder: string;
secondaryPlaceholder: string;
noSubtypesMathingText: string;
subtypeListEmptyText: string;
separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
private searchText = '';
private dirty = false;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private broadcast: BroadcastService,
public translate: TranslateService,
private assetService: AssetService,
private deviceService: DeviceService,
private entityViewService: EntityViewService,
private fb: FormBuilder) {
this.entitySubtypeListFormGroup = this.fb.group({
entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []],
entitySubtype: [null]
});
}
updateValidators() {
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValidators(this.required ? [Validators.required] : []);
this.entitySubtypeListFormGroup.get('entitySubtypeList').updateValueAndValidity();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
switch (this.entityType) {
case EntityType.ASSET:
this.placeholder = this.required ? this.translate.instant('asset.enter-asset-type')
: this.translate.instant('asset.any-asset');
this.secondaryPlaceholder = '+' + this.translate.instant('asset.asset-type');
this.noSubtypesMathingText = 'asset.no-asset-types-matching';
this.subtypeListEmptyText = 'asset.asset-type-list-empty';
this.broadcastSubscription = this.broadcast.on('assetSaved', () => {
this.entitySubtypes = null;
});
break;
case EntityType.DEVICE:
this.placeholder = this.required ? this.translate.instant('device.enter-device-type')
: this.translate.instant('device.any-device');
this.secondaryPlaceholder = '+' + this.translate.instant('device.device-type');
this.noSubtypesMathingText = 'device.no-device-types-matching';
this.subtypeListEmptyText = 'device.device-type-list-empty';
this.broadcastSubscription = this.broadcast.on('deviceSaved', () => {
this.entitySubtypes = null;
});
break;
case EntityType.ENTITY_VIEW:
this.placeholder = this.required ? this.translate.instant('entity-view.enter-entity-view-type')
: this.translate.instant('entity-view.any-entity-view');
this.secondaryPlaceholder = '+' + this.translate.instant('entity-view.entity-view-type');
this.noSubtypesMathingText = 'entity-view.no-entity-view-types-matching';
this.subtypeListEmptyText = 'entity-view.entity-view-type-list-empty';
this.broadcastSubscription = this.broadcast.on('entityViewSaved', () => {
this.entitySubtypes = null;
});
break;
}
this.filteredEntitySubtypeList = this.entitySubtypeListFormGroup.get('entitySubtype').valueChanges
.pipe(
map(value => value ? value : ''),
mergeMap(name => this.fetchEntitySubtypes(name) ),
share()
);
}
ngAfterViewInit(): void {
}
ngOnDestroy(): void {
if (this.broadcastSubscription) {
this.broadcastSubscription.unsubscribe();
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.entitySubtypeListFormGroup.disable({emitEvent: false});
} else {
this.entitySubtypeListFormGroup.enable({emitEvent: false});
}
}
writeValue(value: Array<string> | null): void {
this.searchText = '';
if (value != null && value.length > 0) {
this.modelValue = [...value];
this.entitySubtypeList = [...value];
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList);
} else {
this.entitySubtypeList = [];
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList);
this.modelValue = null;
}
this.dirty = true;
}
add(entitySubtype: string): void {
if (!this.modelValue || this.modelValue.indexOf(entitySubtype) === -1) {
if (!this.modelValue) {
this.modelValue = [];
}
this.modelValue.push(entitySubtype);
this.entitySubtypeList.push(entitySubtype);
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList);
}
this.propagateChange(this.modelValue);
this.clear();
}
chipAdd(event: MatChipInputEvent): void {
const value = event.value;
if ((value || '').trim()) {
this.add(value.trim());
}
this.clear('');
}
remove(entitySubtype: string) {
const index = this.entitySubtypeList.indexOf(entitySubtype);
if (index >= 0) {
this.entitySubtypeList.splice(index, 1);
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList);
this.modelValue.splice(index, 1);
if (!this.modelValue.length) {
this.modelValue = null;
}
this.propagateChange(this.modelValue);
this.clear();
}
}
selected(event: MatAutocompleteSelectedEvent): void {
this.add(event.option.viewValue);
this.clear('');
}
displayEntitySubtypeFn(entitySubtype?: string): string | undefined {
return entitySubtype ? entitySubtype : undefined;
}
fetchEntitySubtypes(searchText?: string): Observable<Array<string>> {
this.searchText = searchText;
return this.getEntitySubtypes().pipe(
map(subTypes => {
let result = subTypes.filter( subType => {
return searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true;
});
if (!result.length) {
result = [searchText];
}
return result;
})
);
}
getEntitySubtypes(): Observable<Array<string>> {
if (!this.entitySubtypes) {
let subTypesObservable: Observable<Array<EntitySubtype>>;
switch (this.entityType) {
case EntityType.ASSET:
subTypesObservable = this.assetService.getAssetTypes(false, true);
break;
case EntityType.DEVICE:
subTypesObservable = this.deviceService.getDeviceTypes(false, true);
break;
case EntityType.ENTITY_VIEW:
subTypesObservable = this.entityViewService.getEntityViewTypes(false, true);
break;
}
if (subTypesObservable) {
this.entitySubtypes = subTypesObservable.pipe(
map(subTypes => subTypes.map(subType => subType.type)),
publishReplay(1),
refCount()
);
} else {
return throwError(null);
}
}
return this.entitySubtypes;
}
onFocus() {
if (this.dirty) {
this.entitySubtypeListFormGroup.get('entitySubtype').updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
clear(value: string = '') {
this.entitySubtypeInput.nativeElement.value = value;
this.entitySubtypeListFormGroup.get('entitySubtype').patchValue(value, {emitEvent: true});
setTimeout(() => {
this.entitySubtypeInput.nativeElement.blur();
this.entitySubtypeInput.nativeElement.focus();
}, 0);
}
}

54
ui-ngx/src/app/shared/components/entity/entity-type-list.component.html

@ -0,0 +1,54 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field appearance="standard" [formGroup]="entityTypeListFormGroup" class="mat-block">
<mat-chip-list #chipList formControlName="entityTypeList">
<mat-chip
*ngFor="let entityType of entityTypeList"
[selectable]="!disabled"
[removable]="!disabled"
(removed)="remove(entityType)">
{{entityType.name}}
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
</mat-chip>
<input matInput type="text" placeholder="{{ !disabled ? ((!entityTypeList || !entityTypeList.length) ? placeholder : secondaryPlaceholder) : '' }}"
style="max-width: 200px;"
#entityTypeInput
(focusin)="onFocus()"
formControlName="entityType"
matAutocompleteOrigin
#origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin"
[matAutocomplete]="entityTypeAutocomplete"
[matChipInputFor]="chipList">
</mat-chip-list>
<mat-autocomplete #entityTypeAutocomplete="matAutocomplete"
class="tb-autocomplete"
[displayWith]="displayEntityTypeFn">
<mat-option *ngFor="let entityType of filteredEntityTypeList | async" [value]="entityType">
<span [innerHTML]="entityType.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredEntityTypeList | async)?.length" [value]="null">
<span>
{{ translate.get('entity.no-entity-types-matching', {entityType: searchText}) | async }}
</span>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="entityTypeListFormGroup.get('entityTypeList').hasError('required')">
{{ 'entity.entity-type-list-empty' | translate }}
</mat-error>
</mat-form-field>

262
ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts

@ -0,0 +1,262 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterContentInit,
AfterViewInit,
Component,
ElementRef,
forwardRef,
Input,
OnInit,
SkipSelf,
ViewChild
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NG_VALUE_ACCESSOR, NgForm, Validators
} from '@angular/forms';
import {Observable, of} from 'rxjs';
import {map, mergeMap, startWith, tap, share, pairwise, filter} from 'rxjs/operators';
import {Store} from '@ngrx/store';
import {AppState} from '@app/core/core.state';
import {TranslateService} from '@ngx-translate/core';
import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import {BaseData} from '@shared/models/base-data';
import {EntityId} from '@shared/models/id/entity-id';
import {EntityService} from '@core/http/entity.service';
import {ErrorStateMatcher, MatAutocomplete, MatAutocompleteSelectedEvent, MatChipList} from '@angular/material';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { emptyPageData } from '@shared/models/page/page-data';
interface EntityTypeInfo {
name: string;
value: EntityType;
}
@Component({
selector: 'tb-entity-type-list',
templateUrl: './entity-type-list.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EntityTypeListComponent),
multi: true
}
]
})
export class EntityTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit {
entityTypeListFormGroup: FormGroup;
modelValue: Array<EntityType> | null;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
const newVal = coerceBooleanProperty(value);
if (this.requiredValue !== newVal) {
this.requiredValue = newVal;
this.updateValidators();
}
}
@Input()
disabled: boolean;
@Input()
allowedEntityTypes: Array<EntityType | AliasEntityType>;
@Input()
ignoreAuthorityFilter: boolean;
@ViewChild('entityTypeInput', {static: false}) entityTypeInput: ElementRef<HTMLInputElement>;
@ViewChild('entityTypeAutocomplete', {static: false}) entityTypeAutocomplete: MatAutocomplete;
@ViewChild('chipList', {static: true}) chipList: MatChipList;
allEntityTypeList: Array<EntityTypeInfo> = [];
entityTypeList: Array<EntityTypeInfo> = [];
filteredEntityTypeList: Observable<Array<EntityTypeInfo>>;
placeholder: string;
secondaryPlaceholder: string;
private searchText = '';
private dirty = false;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
public translate: TranslateService,
private entityService: EntityService,
private fb: FormBuilder) {
this.entityTypeListFormGroup = this.fb.group({
entityTypeList: [this.entityTypeList, this.required ? [Validators.required] : []],
entityType: [null]
});
}
updateValidators() {
this.entityTypeListFormGroup.get('entityTypeList').setValidators(this.required ? [Validators.required] : []);
this.entityTypeListFormGroup.get('entityTypeList').updateValueAndValidity();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.placeholder = this.required ? this.translate.instant('entity.enter-entity-type')
: this.translate.instant('entity.any-entity');
this.secondaryPlaceholder = '+' + this.translate.instant('entity.entity-type');
let entityTypes: Array<EntityType | AliasEntityType>;
if (this.ignoreAuthorityFilter && this.allowedEntityTypes
&& this.allowedEntityTypes.length) {
entityTypes = [];
this.allowedEntityTypes.forEach((entityTypeValue) => {
entityTypes.push(entityTypeValue);
});
} else {
entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes) as Array<EntityType>;
}
entityTypes.forEach((entityType) => {
this.allEntityTypeList.push({
name: this.translate.instant(entityTypeTranslations.get(entityType).type),
value: entityType as EntityType
});
});
this.filteredEntityTypeList = this.entityTypeListFormGroup.get('entityType').valueChanges
.pipe(
// startWith<string | BaseData<EntityId>>(''),
tap((value) => {
if (value && typeof value !== 'string') {
this.add(value);
} else if (value === null) {
this.clear(this.entityTypeInput.nativeElement.value);
}
}),
filter((value) => typeof value === 'string'),
map((value) => value ? (typeof value === 'string' ? value : value.name) : ''),
mergeMap(name => this.fetchEntityTypes(name) ),
share()
);
}
ngAfterViewInit(): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.entityTypeListFormGroup.disable({emitEvent: false});
} else {
this.entityTypeListFormGroup.enable({emitEvent: false});
}
}
writeValue(value: Array<EntityType> | null): void {
this.searchText = '';
if (value != null && value.length > 0) {
this.modelValue = [...value];
this.entityTypeList = [];
value.forEach((entityType) => {
this.entityTypeList.push({
name: this.translate.instant(entityTypeTranslations.get(entityType).type),
value: entityType
});
});
this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList);
} else {
this.entityTypeList = [];
this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList);
this.modelValue = null;
}
this.dirty = true;
}
add(entityType: EntityTypeInfo): void {
if (!this.modelValue || this.modelValue.indexOf(entityType.value) === -1) {
if (!this.modelValue) {
this.modelValue = [];
}
this.modelValue.push(entityType.value);
this.entityTypeList.push(entityType);
this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList);
}
this.propagateChange(this.modelValue);
this.clear();
}
remove(entityType: EntityTypeInfo) {
const index = this.entityTypeList.indexOf(entityType);
if (index >= 0) {
this.entityTypeList.splice(index, 1);
this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList);
this.modelValue.splice(index, 1);
if (!this.modelValue.length) {
this.modelValue = null;
}
this.propagateChange(this.modelValue);
this.clear();
}
}
displayEntityTypeFn(entityType?: EntityTypeInfo): string | undefined {
return entityType ? entityType.name : undefined;
}
fetchEntityTypes(searchText?: string): Observable<Array<EntityTypeInfo>> {
this.searchText = searchText;
let result = this.allEntityTypeList;
if (searchText && searchText.length) {
result = this.allEntityTypeList.filter((entityTypeInfo) => entityTypeInfo.name.toLowerCase().includes(searchText.toLowerCase()));
}
return of(result);
}
onFocus() {
if (this.dirty) {
this.entityTypeListFormGroup.get('entityType').updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
clear(value: string = '') {
this.entityTypeInput.nativeElement.value = value;
this.entityTypeListFormGroup.get('entityType').patchValue(value, {emitEvent: true});
setTimeout(() => {
this.entityTypeInput.nativeElement.blur();
this.entityTypeInput.nativeElement.focus();
}, 0);
}
}

2
ui-ngx/src/app/shared/components/entity/entity-type-select.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field [formGroup]="entityTypeFormGroup" class="mat-block"> <mat-form-field [formGroup]="entityTypeFormGroup">
<mat-label *ngIf="showLabel">{{ 'entity.type' | translate }}</mat-label> <mat-label *ngIf="showLabel">{{ 'entity.type' | translate }}</mat-label>
<mat-select [required]="required" <mat-select [required]="required"
class="tb-entity-type-select" matInput formControlName="entityType"> class="tb-entity-type-select" matInput formControlName="entityType">

10
ui-ngx/src/app/shared/components/entity/entity-type-select.component.scss

@ -15,3 +15,13 @@
*/ */
:host { :host {
} }
:host ::ng-deep {
.mat-form-field-infix {
width: 100%;
}
.mat-select-value {
max-width: 200px;
min-width: 100px;
}
}

8
ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts

@ -45,8 +45,14 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit,
@Input() @Input()
useAliasEntityTypes: boolean; useAliasEntityTypes: boolean;
private showLabelValue: boolean;
get showLabel(): boolean {
return this.showLabelValue;
}
@Input() @Input()
showLabel: boolean; set showLabel(value: boolean) {
this.showLabelValue = coerceBooleanProperty(value);
}
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {

67
ui-ngx/src/app/shared/components/tb-error.component.ts

@ -0,0 +1,67 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Input } from '@angular/core';
import { trigger, state, style, transition, animate } from '@angular/animations';
@Component({
selector: 'tb-error',
template: `
<div [@animation]="state" style="margin-top:0.5rem;font-size:.75rem">
<mat-error >
{{message}}
</mat-error>
</div>
`,
styles: [`
:host {
height: 24px;
}
`],
animations: [
trigger('animation', [
state('show', style({
opacity: 1,
})),
state('hide', style({
opacity: 0,
transform: 'translateY(-1rem)'
})),
transition('show => hide', animate('200ms ease-out')),
transition('* => show', animate('200ms ease-in'))
]),
]
})
export class TbErrorComponent {
errorValue: any;
state: any;
message;
@Input()
set error(value) {
if (value && !this.message) {
this.message = value;
this.state = 'hide';
setTimeout(() => {
this.state = 'show';
});
} else {
this.errorValue = value;
this.state = value ? 'show' : 'hide';
}
}
}

96
ui-ngx/src/app/shared/models/alias.models.ts

@ -14,7 +14,10 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { EntityType } from '@shared/models/entity-type.models'; import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
import { EntityId } from '@shared/models/id/entity-id';
import { EntitySearchDirection, EntityTypeFilter } from '@shared/models/relation.models';
import { EntityInfo } from './entity.models';
export enum AliasFilterType { export enum AliasFilterType {
singleEntity = 'singleEntity', singleEntity = 'singleEntity',
@ -46,15 +49,87 @@ export const aliasFilterTypeTranslationMap = new Map<AliasFilterType, string>(
] ]
); );
export interface EntityAliasFilter { export interface SingleEntityFilter {
type: AliasFilterType; singleEntity?: EntityId;
entityType: EntityType; }
resolveMultiple: boolean;
export interface EntityListFilter {
entityType?: EntityType;
entityList?: string[]; entityList?: string[];
}
export interface EntityNameFilter {
entityType?: EntityType;
entityNameFilter?: string; entityNameFilter?: string;
[key: string]: any; }
// TODO:
export interface StateEntityFilter {
stateEntityParamName?: string;
defaultStateEntity?: EntityId;
}
export interface AssetTypeFilter {
assetType?: string;
assetNameFilter?: string;
}
export interface DeviceTypeFilter {
deviceType?: string;
deviceNameFilter?: string;
}
export interface EntityViewFilter {
entityViewType?: string;
entityViewNameFilter?: string;
}
export interface RelationsQueryFilter {
rootStateEntity?: boolean;
stateEntityParamName?: string;
defaultStateEntity?: EntityId;
rootEntity?: EntityId;
direction?: EntitySearchDirection;
filters?: Array<EntityTypeFilter>;
maxLevel?: number;
}
export interface EntitySearchQueryFilter {
rootStateEntity?: boolean;
stateEntityParamName?: string;
defaultStateEntity?: EntityId;
rootEntity?: EntityId;
relationType?: string;
direction?: EntitySearchDirection;
}
export interface AssetSearchQueryFilter extends EntitySearchQueryFilter {
assetTypes?: string[];
}
export interface DeviceSearchQueryFilter extends EntitySearchQueryFilter {
deviceTypes?: string[];
}
export interface EntityViewSearchQueryFilter extends EntitySearchQueryFilter {
entityViewTypes?: string[];
}
export type EntityFilters =
SingleEntityFilter &
EntityListFilter &
EntityNameFilter &
StateEntityFilter &
AssetTypeFilter &
DeviceTypeFilter &
EntityViewFilter &
RelationsQueryFilter &
AssetSearchQueryFilter &
DeviceSearchQueryFilter &
EntityViewSearchQueryFilter;
export interface EntityAliasFilter extends EntityFilters {
type?: AliasFilterType;
resolveMultiple: boolean;
} }
export interface EntityAlias { export interface EntityAlias {
@ -62,9 +137,14 @@ export interface EntityAlias {
alias: string; alias: string;
filter: EntityAliasFilter; filter: EntityAliasFilter;
[key: string]: any; [key: string]: any;
// TODO:
} }
export interface EntityAliases { export interface EntityAliases {
[id: string]: EntityAlias; [id: string]: EntityAlias;
} }
export interface EntityAliasFilterResult {
entities: Array<EntityInfo>;
stateEntity: boolean;
entityParamName?: string;
}

5
ui-ngx/src/app/shared/models/asset.models.ts

@ -19,6 +19,7 @@ import {AssetId} from './id/asset-id';
import {TenantId} from '@shared/models/id/tenant-id'; import {TenantId} from '@shared/models/id/tenant-id';
import {CustomerId} from '@shared/models/id/customer-id'; import {CustomerId} from '@shared/models/id/customer-id';
import {DeviceCredentialsId} from '@shared/models/id/device-credentials-id'; import {DeviceCredentialsId} from '@shared/models/id/device-credentials-id';
import { EntitySearchQuery } from '@shared/models/relation.models';
export interface Asset extends BaseData<AssetId> { export interface Asset extends BaseData<AssetId> {
tenantId: TenantId; tenantId: TenantId;
@ -32,3 +33,7 @@ export interface AssetInfo extends Asset {
customerTitle: string; customerTitle: string;
customerIsPublic: boolean; customerIsPublic: boolean;
} }
export interface AssetSearchQuery extends EntitySearchQuery {
assetTypes: Array<string>;
}

5
ui-ngx/src/app/shared/models/device.models.ts

@ -19,6 +19,7 @@ import {DeviceId} from './id/device-id';
import {TenantId} from '@shared/models/id/tenant-id'; import {TenantId} from '@shared/models/id/tenant-id';
import {CustomerId} from '@shared/models/id/customer-id'; import {CustomerId} from '@shared/models/id/customer-id';
import {DeviceCredentialsId} from '@shared/models/id/device-credentials-id'; import {DeviceCredentialsId} from '@shared/models/id/device-credentials-id';
import { EntitySearchQuery } from '@shared/models/relation.models';
export interface Device extends BaseData<DeviceId> { export interface Device extends BaseData<DeviceId> {
tenantId: TenantId; tenantId: TenantId;
@ -52,3 +53,7 @@ export interface DeviceCredentials extends BaseData<DeviceCredentialsId> {
credentialsId: string; credentialsId: string;
credentialsValue: string; credentialsValue: string;
} }
export interface DeviceSearchQuery extends EntitySearchQuery {
deviceTypes: Array<string>;
}

5
ui-ngx/src/app/shared/models/entity-view.models.ts

@ -20,6 +20,7 @@ import {TenantId} from '@shared/models/id/tenant-id';
import {CustomerId} from '@shared/models/id/customer-id'; import {CustomerId} from '@shared/models/id/customer-id';
import {EntityViewId} from '@shared/models/id/entity-view-id'; import {EntityViewId} from '@shared/models/id/entity-view-id';
import {EntityId} from '@shared/models/id/entity-id'; import {EntityId} from '@shared/models/id/entity-id';
import { EntitySearchQuery } from '@shared/models/relation.models';
export interface AttributesEntityView { export interface AttributesEntityView {
cs: Array<string>; cs: Array<string>;
@ -48,3 +49,7 @@ export interface EntityViewInfo extends EntityView {
customerTitle: string; customerTitle: string;
customerIsPublic: boolean; customerIsPublic: boolean;
} }
export interface EntityViewSearchQuery extends EntitySearchQuery {
entityViewTypes: Array<string>;
}

9
ui-ngx/src/app/shared/models/relation.models.ts

@ -62,8 +62,8 @@ export interface RelationsSearchParameters {
rootId: string; rootId: string;
rootType: EntityType; rootType: EntityType;
direction: EntitySearchDirection; direction: EntitySearchDirection;
relationTypeGroup: RelationTypeGroup; relationTypeGroup?: RelationTypeGroup;
maxLevel: number; maxLevel?: number;
} }
export interface EntityRelationsQuery { export interface EntityRelationsQuery {
@ -71,6 +71,11 @@ export interface EntityRelationsQuery {
filters: Array<EntityTypeFilter>; filters: Array<EntityTypeFilter>;
} }
export interface EntitySearchQuery {
parameters: RelationsSearchParameters;
relationType: string;
}
export interface EntityRelation { export interface EntityRelation {
from: EntityId; from: EntityId;
to: EntityId; to: EntityId;

9
ui-ngx/src/app/shared/shared.module.ts

@ -96,6 +96,9 @@ import { DashboardSelectPanelComponent } from '@shared/components/dashboard-sele
import { DashboardSelectComponent } from '@shared/components/dashboard-select.component'; import { DashboardSelectComponent } from '@shared/components/dashboard-select.component';
import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component'; import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component';
import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe'; import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe';
import { TbErrorComponent } from './components/tb-error.component';
import { EntityTypeListComponent } from './components/entity/entity-type-list.component';
import { EntitySubTypeListComponent } from './components/entity/entity-subtype-list.component';
@NgModule({ @NgModule({
providers: [ providers: [
@ -122,6 +125,7 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe';
HelpComponent, HelpComponent,
TbCheckboxComponent, TbCheckboxComponent,
TbSnackBarComponent, TbSnackBarComponent,
TbErrorComponent,
BreadcrumbComponent, BreadcrumbComponent,
UserMenuComponent, UserMenuComponent,
TimewindowComponent, TimewindowComponent,
@ -135,12 +139,14 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe';
DashboardAutocompleteComponent, DashboardAutocompleteComponent,
EntitySubTypeAutocompleteComponent, EntitySubTypeAutocompleteComponent,
EntitySubTypeSelectComponent, EntitySubTypeSelectComponent,
EntitySubTypeListComponent,
EntityAutocompleteComponent, EntityAutocompleteComponent,
EntityListComponent, EntityListComponent,
EntityTypeSelectComponent, EntityTypeSelectComponent,
EntitySelectComponent, EntitySelectComponent,
EntityKeysListComponent, EntityKeysListComponent,
EntityListSelectComponent, EntityListSelectComponent,
EntityTypeListComponent,
RelationTypeAutocompleteComponent, RelationTypeAutocompleteComponent,
SocialSharePanelComponent, SocialSharePanelComponent,
JsonObjectEditComponent, JsonObjectEditComponent,
@ -207,6 +213,7 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe';
TbAnchorComponent, TbAnchorComponent,
HelpComponent, HelpComponent,
TbCheckboxComponent, TbCheckboxComponent,
TbErrorComponent,
BreadcrumbComponent, BreadcrumbComponent,
UserMenuComponent, UserMenuComponent,
TimewindowComponent, TimewindowComponent,
@ -218,12 +225,14 @@ import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe';
DashboardAutocompleteComponent, DashboardAutocompleteComponent,
EntitySubTypeAutocompleteComponent, EntitySubTypeAutocompleteComponent,
EntitySubTypeSelectComponent, EntitySubTypeSelectComponent,
EntitySubTypeListComponent,
EntityAutocompleteComponent, EntityAutocompleteComponent,
EntityListComponent, EntityListComponent,
EntityTypeSelectComponent, EntityTypeSelectComponent,
EntitySelectComponent, EntitySelectComponent,
EntityKeysListComponent, EntityKeysListComponent,
EntityListSelectComponent, EntityListSelectComponent,
EntityTypeListComponent,
RelationTypeAutocompleteComponent, RelationTypeAutocompleteComponent,
SocialSharePanelComponent, SocialSharePanelComponent,
JsonObjectEditComponent, JsonObjectEditComponent,

18
ui-ngx/src/styles.scss

@ -246,6 +246,15 @@ div {
color: #808080; color: #808080;
} }
.mat-caption {
&.tb-required::after {
font-size: 10px;
color: rgba(0, 0, 0, .54);
vertical-align: top;
content: " *";
}
}
pre.tb-highlight { pre.tb-highlight {
display: block; display: block;
padding: 15px; padding: 15px;
@ -303,6 +312,15 @@ pre.tb-highlight {
margin-top: 32px; margin-top: 32px;
} }
.tb-prompt {
display: flex;
font-size: 18px;
font-weight: 400;
line-height: 18px;
color: rgba(0, 0, 0, .38);
text-transform: uppercase;
}
.tb-fullscreen { .tb-fullscreen {
position: fixed !important; position: fixed !important;
top: 0; top: 0;

Loading…
Cancel
Save