From b9c27ed52bf8ee12ec0b74138223f9f8eac87d91 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 11 Oct 2019 19:22:03 +0300 Subject: [PATCH] Implement Alias controller. Datasources implementation. --- ui-ngx/src/app/core/api/alias-controller.ts | 285 +++++++++++++++++- .../app/core/api/datasource-subcription.ts | 6 +- ui-ngx/src/app/core/api/widget-api.models.ts | 23 +- .../src/app/core/api/widget-subscription.ts | 18 +- ui-ngx/src/app/core/http/entity.service.ts | 73 ++++- ui-ngx/src/app/core/services/utils.service.ts | 58 +++- .../alias/entity-alias-dialog.component.ts | 2 +- .../alias/entity-alias-select.component.html | 60 ++++ .../entity-alias-select.component.models.ts | 23 ++ .../alias/entity-alias-select.component.scss | 24 ++ .../alias/entity-alias-select.component.ts | 258 ++++++++++++++++ .../alias/entity-aliases-dialog.component.ts | 2 + .../home/components/home-components.module.ts | 7 +- .../widget/data-keys.component.models.ts | 22 ++ .../widget/widget-config.component.html | 49 ++- .../widget/widget-config.component.models.ts | 20 ++ .../widget/widget-config.component.scss | 47 ++- .../widget/widget-config.component.ts | 153 +++++++++- .../components/widget/widget.component.ts | 27 +- .../dashboard/dashboard-page.component.ts | 2 +- .../dashboard/edit-widget.component.html | 1 + .../pages/dashboard/edit-widget.component.ts | 38 ++- ui-ngx/src/app/shared/models/alias.models.ts | 2 +- ui-ngx/src/app/shared/models/widget.models.ts | 13 + ui-ngx/src/app/shared/pipe/truncate.pipe.ts | 42 +++ ui-ngx/src/app/shared/shared.module.ts | 6 +- ui-ngx/src/styles.scss | 11 + 27 files changed, 1195 insertions(+), 77 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html create mode 100644 ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts create mode 100644 ui-ngx/src/app/shared/pipe/truncate.pipe.ts diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index 056efc83cf..07ecb519eb 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -14,15 +14,16 @@ /// limitations under the License. /// -import { IAliasController, AliasInfo, IStateController } from '@core/api/widget-api.models'; -import { Observable, of, Subject } from 'rxjs'; -import { Datasource } from '@app/shared/models/widget.models'; +import { AliasInfo, IAliasController, IStateController, StateEntityInfo, StateControllerHolder } from '@core/api/widget-api.models'; +import { forkJoin, Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { DataKey, Datasource, DatasourceType } from '@app/shared/models/widget.models'; import { deepClone } from '@core/utils'; import { EntityService } from '@core/http/entity.service'; import { UtilsService } from '@core/services/utils.service'; import { EntityAliases } from '@shared/models/alias.models'; import { EntityInfo } from '@shared/models/entity.models'; import * as equal from 'deep-equal'; +import { map, tap } from 'rxjs/operators'; export class DummyAliasController implements IAliasController { @@ -40,6 +41,10 @@ export class DummyAliasController implements IAliasController { return of(null); } + resolveAlarmSource(alarmSource: Datasource): Observable { + return of(deepClone(alarmSource)); + } + resolveDatasources(datasources: Array): Observable> { return of(deepClone(datasources)); } @@ -61,6 +66,10 @@ export class DummyAliasController implements IAliasController { dashboardStateChanged() { } + getEntityAliasId(aliasName: string): string { + return null; + } + } export class AliasController implements IAliasController { @@ -74,28 +83,279 @@ export class AliasController implements IAliasController { entityAliases: EntityAliases; resolvedAliases: {[aliasId: string]: AliasInfo} = {}; + resolvedAliasesObservable: {[aliasId: string]: Observable} = {}; - [key: string]: any | null; + resolvedAliasesToStateEntities: {[aliasId: string]: StateEntityInfo} = {}; constructor(private utils: UtilsService, private entityService: EntityService, - private stateController: IStateController, + private stateControllerHolder: StateControllerHolder, private origEntityAliases: EntityAliases) { this.entityAliases = deepClone(this.origEntityAliases); } - getAliasInfo(aliasId: string): Observable { - return of(null); + + updateEntityAliases(newEntityAliases: EntityAliases) { + const changedAliasIds: Array = []; + for (const aliasId of Object.keys(newEntityAliases)) { + const newEntityAlias = newEntityAliases[aliasId]; + const prevEntityAlias = this.entityAliases[aliasId]; + if (!equal(newEntityAlias, prevEntityAlias)) { + changedAliasIds.push(aliasId); + this.setAliasUnresolved(aliasId); + } + } + for (const aliasId of Object.keys(this.entityAliases)) { + if (!newEntityAliases[aliasId]) { + changedAliasIds.push(aliasId); + this.setAliasUnresolved(aliasId); + } + } + this.entityAliases = deepClone(newEntityAliases); + if (changedAliasIds.length) { + this.entityAliasesChangedSubject.next(changedAliasIds); + } } - resolveDatasources(datasources: Array): Observable> { - return of(deepClone(datasources)); + dashboardStateChanged() { + const changedAliasIds: Array = []; + for (const aliasId of Object.keys(this.resolvedAliasesToStateEntities)) { + const stateEntityInfo = this.resolvedAliasesToStateEntities[aliasId]; + const newEntityId = this.stateControllerHolder().getEntityId(stateEntityInfo.entityParamName); + const prevEntityId = stateEntityInfo.entityId; + if (!equal(newEntityId, prevEntityId)) { + changedAliasIds.push(aliasId); + this.setAliasUnresolved(aliasId); + } + } + if (changedAliasIds.length) { + this.entityAliasesChangedSubject.next(changedAliasIds); + } + } + + private setAliasUnresolved(aliasId: string) { + delete this.resolvedAliases[aliasId]; + delete this.resolvedAliasesObservable[aliasId]; + delete this.resolvedAliasesToStateEntities[aliasId]; } getEntityAliases(): EntityAliases { return this.entityAliases; } + getEntityAliasId(aliasName: string): string { + for (const aliasId of Object.keys(this.entityAliases)) { + const alias = this.entityAliases[aliasId]; + if (alias.alias === aliasName) { + return aliasId; + } + } + return null; + } + + getAliasInfo(aliasId: string): Observable { + const aliasInfo = this.resolvedAliases[aliasId]; + if (aliasInfo) { + return of(aliasInfo); + } else if (this.resolvedAliasesObservable[aliasId]) { + return this.resolvedAliasesObservable[aliasId]; + } else { + const resolvedAliasSubject = new ReplaySubject(); + this.resolvedAliasesObservable[aliasId] = resolvedAliasSubject.asObservable(); + const entityAlias = this.entityAliases[aliasId]; + if (entityAlias) { + this.entityService.resolveAlias(entityAlias, this.stateControllerHolder().getStateParams()).subscribe( + (resolvedAliasInfo) => { + this.resolvedAliases[aliasId] = resolvedAliasInfo; + delete this.resolvedAliasesObservable[aliasId]; + if (resolvedAliasInfo.stateEntity) { + const stateEntityInfo: StateEntityInfo = { + entityParamName: resolvedAliasInfo.entityParamName, + entityId: this.stateControllerHolder().getEntityId(resolvedAliasInfo.entityParamName) + }; + this.resolvedAliasesToStateEntities[aliasId] = stateEntityInfo; + } + this.entityAliasResolvedSubject.next(aliasId); + resolvedAliasSubject.next(resolvedAliasInfo); + resolvedAliasSubject.complete(); + }, + () => { + resolvedAliasSubject.error(null); + delete this.resolvedAliasesObservable[aliasId]; + } + ); + } else { + resolvedAliasSubject.error(null); + delete this.resolvedAliasesObservable[aliasId]; + } + return this.resolvedAliasesObservable[aliasId]; + } + } + + private resolveDatasource(datasource: Datasource, isSingle?: boolean): Observable> { + if (datasource.type === DatasourceType.entity) { + if (datasource.entityAliasId) { + return this.getAliasInfo(datasource.entityAliasId).pipe( + map((aliasInfo) => { + datasource.aliasName = aliasInfo.alias; + if (aliasInfo.resolveMultiple && !isSingle) { + let newDatasource: Datasource; + const resolvedEntities = aliasInfo.resolvedEntities; + if (resolvedEntities && resolvedEntities.length) { + const datasources: Array = []; + for (let i = 0; i < resolvedEntities.length; i++) { + const resolvedEntity = resolvedEntities[i]; + newDatasource = deepClone(datasource); + if (resolvedEntity.origEntity) { + newDatasource.entity = deepClone(resolvedEntity.origEntity); + } else { + newDatasource.entity = {}; + } + newDatasource.entityId = resolvedEntity.id; + newDatasource.entityType = resolvedEntity.entityType; + newDatasource.entityName = resolvedEntity.name; + newDatasource.entityLabel = resolvedEntity.label; + newDatasource.entityDescription = resolvedEntity.entityDescription; + newDatasource.name = resolvedEntity.name; + newDatasource.generated = i > 0 ? true : false; + datasources.push(newDatasource); + } + return datasources; + } else { + if (aliasInfo.stateEntity) { + newDatasource = deepClone(datasource); + newDatasource.unresolvedStateEntity = true; + return [newDatasource]; + } else { + return []; + // throw new Error('Unable to resolve datasource.'); + } + } + } else { + const entity = aliasInfo.currentEntity; + if (entity) { + if (entity.origEntity) { + datasource.entity = deepClone(entity.origEntity); + } else { + datasource.entity = {}; + } + datasource.entityId = entity.id; + datasource.entityType = entity.entityType; + datasource.entityName = entity.name; + datasource.entityLabel = entity.label; + datasource.name = entity.name; + datasource.entityDescription = entity.entityDescription; + return [datasource]; + } else { + if (aliasInfo.stateEntity) { + datasource.unresolvedStateEntity = true; + return [datasource]; + } else { + return []; + // throw new Error('Unable to resolve datasource.'); + } + } + } + }) + ); + } else { + datasource.aliasName = datasource.entityName; + datasource.name = datasource.entityName; + return of([datasource]); + } + } else { + return of([datasource]); + } + } + + resolveAlarmSource(alarmSource: Datasource): Observable { + return this.resolveDatasource(alarmSource, true).pipe( + map((datasources) => { + const datasource = datasources[0]; + if (datasource.type === DatasourceType.function) { + let name: string; + if (datasource.name && datasource.name.length) { + name = datasource.name; + } else { + name = DatasourceType.function; + } + datasource.name = name; + datasource.aliasName = name; + datasource.entityName = name; + } else if (datasource.unresolvedStateEntity) { + datasource.name = 'Unresolved'; + datasource.entityName = 'Unresolved'; + } + return datasource; + }) + ); + } + + resolveDatasources(datasources: Array): Observable> { + const newDatasources = deepClone(datasources); + const observables = new Array>>(); + newDatasources.forEach((datasource) => { + observables.push(this.resolveDatasource(datasource)); + }); + return forkJoin(observables).pipe( + map((arrayOfDatasources) => { + const result = new Array(); + arrayOfDatasources.forEach((datasourcesArray) => { + result.push(...datasourcesArray); + }); + result.sort((d1, d2) => { + const i1 = d1.generated ? 1 : 0; + const i2 = d2.generated ? 1 : 0; + return i1 - i2; + }); + let index = 0; + let functionIndex = 0; + result.forEach((datasource) => { + if (datasource.type === DatasourceType.function) { + let name: string; + if (datasource.name && datasource.name.length) { + name = datasource.name; + } else { + functionIndex++; + name = DatasourceType.function; + if (functionIndex > 1) { + name += ' ' + functionIndex; + } + } + datasource.name = name; + datasource.aliasName = name; + datasource.entityName = name; + } else if (datasource.unresolvedStateEntity) { + datasource.name = 'Unresolved'; + datasource.entityName = 'Unresolved'; + } + datasource.dataKeys.forEach((dataKey) => { + if (datasource.generated) { + dataKey._hash = Math.random(); + dataKey.color = this.utils.getMaterialColor(index); + } + index++; + }); + this.updateDatasourceKeyLabels(datasource); + }); + return result; + }) + ); + } + + private updateDatasourceKeyLabels(datasource: Datasource) { + datasource.dataKeys.forEach((dataKey) => { + this.updateDataKeyLabel(dataKey, datasource); + }); + } + + private updateDataKeyLabel(dataKey: DataKey, datasource: Datasource) { + if (!dataKey.pattern) { + dataKey.pattern = deepClone(dataKey.label); + } + dataKey.label = this.utils.createLabelFromDatasource(datasource, dataKey.pattern); + } + getInstantAliasInfo(aliasId: string): AliasInfo { return this.resolvedAliases[aliasId]; } @@ -110,11 +370,4 @@ export class AliasController implements IAliasController { } } } - - updateEntityAliases(entityAliases: EntityAliases) { - } - - dashboardStateChanged() { - } - } diff --git a/ui-ngx/src/app/core/api/datasource-subcription.ts b/ui-ngx/src/app/core/api/datasource-subcription.ts index 982215a50b..de8ffe22a3 100644 --- a/ui-ngx/src/app/core/api/datasource-subcription.ts +++ b/ui-ngx/src/app/core/api/datasource-subcription.ts @@ -268,11 +268,11 @@ export class DatasourceSubscription { data = subscriptionUpdate; } } else { - for (const key of Object.keys(data.data)) { - const keyData = data.data[key]; + for (const key of Object.keys(subscriptionUpdate.data)) { + const keyData = subscriptionUpdate.data[key]; keyData.sort((set1, set2) => set1[0] - set2[0]); } - this.onData(data.data, DataKeyType.timeseries, true); + this.onData(subscriptionUpdate.data, DataKeyType.timeseries, true); } } } diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 934315e678..2d501c74e6 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -73,22 +73,27 @@ export interface AliasInfo { currentEntity?: EntityInfo; selectedId?: string; resolvedEntities?: Array; - [key: string]: any | null; - // TODO: + entityParamName?: string; + resolveMultiple?: boolean; +} + +export interface StateEntityInfo { + entityParamName: string; + entityId: EntityId; } export interface IAliasController { entityAliasesChanged: Observable>; entityAliasResolved: Observable; getAliasInfo(aliasId: string): Observable; + getEntityAliasId(aliasName: string): string; getInstantAliasInfo(aliasId: string): AliasInfo; resolveDatasources(datasources: Array): Observable>; + resolveAlarmSource(alarmSource: Datasource): Observable; getEntityAliases(): EntityAliases; updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo); updateEntityAliases(entityAliases: EntityAliases); dashboardStateChanged(); - [key: string]: any | null; - // TODO: } export interface StateObject { @@ -103,6 +108,8 @@ export interface StateParams { [key: string]: any | null; } +export type StateControllerHolder = () => IStateController; + export interface IStateController { getStateParams(): StateParams; getStateParamsByStateId(stateId: string): StateParams; @@ -131,7 +138,11 @@ export interface SubscriptionInfo { attributes?: Array; functions?: Array; alarmFields?: Array; - [key: string]: any; + + deviceId?: string; + deviceName?: string; + deviceNamePrefix?: string; + deviceIds?: Array; } export interface WidgetSubscriptionContext { @@ -178,8 +189,6 @@ export interface WidgetSubscriptionOptions { decimals?: number; units?: string; callbacks?: WidgetSubscriptionCallbacks; - [key: string]: any; - // TODO: } export interface SubscriptionEntityInfo { diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index f6ff935349..49b643886b 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -206,8 +206,8 @@ export class WidgetSubscription implements IWidgetSubscription { subscriptionSubject.next(this); subscriptionSubject.complete(); }, - () => { - subscriptionSubject.error(null); + (err) => { + subscriptionSubject.error(err); }); } } @@ -279,8 +279,8 @@ export class WidgetSubscription implements IWidgetSubscription { initDataSubscriptionSubject.next(); initDataSubscriptionSubject.complete(); }, - () => { - initDataSubscriptionSubject.error(null); + (err) => { + initDataSubscriptionSubject.error(err); } ); } @@ -462,6 +462,7 @@ export class WidgetSubscription implements IWidgetSubscription { } } let index = 0; + let forceUpdate = !this.datasources.length; this.datasources.forEach((datasource) => { const listener: DatasourceListener = { subscriptionType: this.type, @@ -488,18 +489,17 @@ export class WidgetSubscription implements IWidgetSubscription { if (datasource.dataKeys.length) { this.ctx.datasourceService.subscribeToDatasource(listener); } - let forceUpdate = false; if (datasource.unresolvedStateEntity || !datasource.dataKeys.length || (datasource.type === DatasourceType.entity && !datasource.entityId) ) { forceUpdate = true; } - if (forceUpdate) { - this.notifyDataLoaded(); - this.onDataUpdated(); - } }); + if (forceUpdate) { + this.notifyDataLoaded(); + this.onDataUpdated(); + } } } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 39b07b8fc0..b8a0e200dc 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -40,10 +40,10 @@ import { EntityViewService } from '@core/http/entity-view.service'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { defaultHttpOptions } from '@core/http/http-utils'; import { RuleChainService } from '@core/http/rule-chain.service'; -import { StateParams, SubscriptionInfo } from '@core/api/widget-api.models'; +import { AliasInfo, StateParams, SubscriptionInfo } from '@core/api/widget-api.models'; import { Datasource, DatasourceType, KeyInfo } from '@app/shared/models/widget.models'; import { UtilsService } from '@core/services/utils.service'; -import { AliasFilterType, EntityAliasFilter, EntityAliasFilterResult } from '@shared/models/alias.models'; +import { AliasFilterType, EntityAlias, EntityAliasFilter, EntityAliasFilterResult } from '@shared/models/alias.models'; import { EntityInfo } from '@shared/models/entity.models'; import { EntityRelationInfo, @@ -376,6 +376,55 @@ export class EntityService { return result; } + public filterAliasByEntityTypes(entityAlias: EntityAlias, entityTypes: Array): boolean { + const filter = entityAlias.filter; + if (this.filterAliasFilterTypeByEntityTypes(filter.type, entityTypes)) { + switch (filter.type) { + case AliasFilterType.singleEntity: + return entityTypes.indexOf(filter.singleEntity.entityType) > -1 ? true : false; + case AliasFilterType.entityList: + return entityTypes.indexOf(filter.entityType) > -1 ? true : false; + case AliasFilterType.entityName: + return entityTypes.indexOf(filter.entityType) > -1 ? true : false; + case AliasFilterType.stateEntity: + return true; + case AliasFilterType.assetType: + return entityTypes.indexOf(EntityType.ASSET) > -1 ? true : false; + case AliasFilterType.deviceType: + return entityTypes.indexOf(EntityType.DEVICE) > -1 ? true : false; + case AliasFilterType.entityViewType: + return entityTypes.indexOf(EntityType.ENTITY_VIEW) > -1 ? true : false; + case AliasFilterType.relationsQuery: + if (filter.filters && filter.filters.length) { + let match = false; + for (const relationFilter of filter.filters) { + if (relationFilter.entityTypes && relationFilter.entityTypes.length) { + for (const relationFilterEntityType of relationFilter.entityTypes) { + if (entityTypes.indexOf(relationFilterEntityType) > -1) { + match = true; + break; + } + } + } else { + match = true; + break; + } + } + return match; + } else { + return true; + } + case AliasFilterType.assetSearchQuery: + return entityTypes.indexOf(EntityType.ASSET) > -1 ? true : false; + case AliasFilterType.deviceSearchQuery: + return entityTypes.indexOf(EntityType.DEVICE) > -1 ? true : false; + case AliasFilterType.entityViewSearchQuery: + return entityTypes.indexOf(EntityType.ENTITY_VIEW) > -1 ? true : false; + } + } + return false; + } + private filterAliasFilterTypeByEntityTypes(aliasFilterType: AliasFilterType, entityTypes: Array): boolean { if (!entityTypes || !entityTypes.length) { @@ -512,6 +561,26 @@ export class EntityService { } } + public resolveAlias(entityAlias: EntityAlias, stateParams: StateParams): Observable { + const filter = entityAlias.filter; + return this.resolveAliasFilter(filter, stateParams, -1, false).pipe( + map((result) => { + const aliasInfo: AliasInfo = { + alias: entityAlias.alias, + stateEntity: result.stateEntity, + entityParamName: result.entityParamName, + resolveMultiple: filter.resolveMultiple + }; + aliasInfo.resolvedEntities = result.entities; + aliasInfo.currentEntity = null; + if (aliasInfo.resolvedEntities.length) { + aliasInfo.currentEntity = aliasInfo.resolvedEntities[0]; + } + return aliasInfo; + }) + ); + } + public resolveAliasFilter(filter: EntityAliasFilter, stateParams: StateParams, maxItems: number, failOnEmpty: boolean): Observable { const result: EntityAliasFilterResult = { diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 89afecc718..69bf1328ec 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -17,7 +17,7 @@ import { Inject, Injectable } from '@angular/core'; import { WINDOW } from '@core/services/window.service'; import { ExceptionData } from '@app/shared/models/error.models'; -import { deepClone, isDefined, isUndefined, deleteNullProperties } from '@core/utils'; +import { deepClone, deleteNullProperties, isDefined, isUndefined } from '@core/utils'; import { WindowMessage } from '@shared/models/window-message.model'; import { TranslateService } from '@ngx-translate/core'; import { customTranslationsPrefix } from '@app/shared/models/constants'; @@ -50,6 +50,14 @@ for (const func of Object.keys(predefinedFunctions)) { predefinedFunctionsList.push(func); } +const defaultAlarmFields: Array = [ + alarmFields.createdTime.keyName, + alarmFields.originator.keyName, + alarmFields.type.keyName, + alarmFields.severity.keyName, + alarmFields.status.keyName +]; + @Injectable({ providedIn: 'root' }) @@ -75,6 +83,8 @@ export class UtilsService { dataKeys: [deepClone(this.defaultDataKey)] }; + defaultAlarmDataKeys: Array = []; + constructor(@Inject(WINDOW) private window: Window, private translate: TranslateService) { let frame: Element = null; @@ -109,6 +119,28 @@ export class UtilsService { return datasource; } + private initDefaultAlarmDataKeys() { + for (let i = 0; i < defaultAlarmFields.length; i++) { + const name = defaultAlarmFields[i]; + const dataKey: DataKey = { + name, + type: DataKeyType.alarm, + label: this.translate.instant(alarmFields[name].name), + color: this.getMaterialColor(i), + settings: {}, + _hash: Math.random() + }; + this.defaultAlarmDataKeys.push(dataKey); + } + } + + public getDefaultAlarmDataKeys(): Array { + if (!this.defaultAlarmDataKeys.length) { + this.initDefaultAlarmDataKeys(); + } + return deepClone(this.defaultAlarmDataKeys); + } + public generateObjectFromJsonSchema(schema: any): any { const obj = jsonSchemaDefaults(schema); deleteNullProperties(obj); @@ -292,6 +324,30 @@ export class UtilsService { return dataKey; } + public createLabelFromDatasource(datasource: Datasource, pattern: string) { + let label = deepClone(pattern); + let match = varsRegex.exec(pattern); + while (match !== null) { + const variable = match[0]; + const variableName = match[1]; + if (variableName === 'dsName') { + label = label.split(variable).join(datasource.name); + } else if (variableName === 'entityName') { + label = label.split(variable).join(datasource.entityName); + } else if (variableName === 'deviceName') { + label = label.split(variable).join(datasource.entityName); + } else if (variableName === 'entityLabel') { + label = label.split(variable).join(datasource.entityLabel); + } else if (variableName === 'aliasName') { + label = label.split(variable).join(datasource.aliasName); + } else if (variableName === 'entityDescription') { + label = label.split(variable).join(datasource.entityDescription); + } + match = varsRegex.exec(pattern); + } + return label; + } + public generateColors(datasources: Array) { let index = 0; datasources.forEach((datasource) => { diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts index 535b1d9452..75b24cdf7d 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts @@ -86,7 +86,7 @@ export class EntityAliasDialogComponent extends DialogComponent + + + + + + + + + +
+
+ entity.no-aliases-found +
+ + + {{ translate.get('entity.no-alias-matching', + {alias: truncate.transform(searchText, true, 6, '...')}) | async }} + + + + entity.create-new-alias + +
+
+
+ + {{ 'entity.alias-required' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.models.ts b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.models.ts new file mode 100644 index 0000000000..5f010b8386 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.models.ts @@ -0,0 +1,23 @@ +/// +/// 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 { EntityType } from '@shared/models/entity-type.models'; +import { Observable } from 'rxjs'; +import { EntityAlias } from '@shared/models/alias.models'; + +export interface EntityAliasSelectCallbacks { + createEntityAlias: (alias: string, allowedEntityTypes: Array) => Observable; +} diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss new file mode 100644 index 0000000000..414b397234 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss @@ -0,0 +1,24 @@ +/** + * 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 { + +} + +:host ::ng-deep { + .mat-form-field-infix { + border-top: none; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts new file mode 100644 index 0000000000..16f140e50f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts @@ -0,0 +1,258 @@ +/// +/// 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 { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NG_VALUE_ACCESSOR, NgForm +} from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { map, mergeMap, startWith, tap, share, 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} 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 {coerceBooleanProperty} from '@angular/cdk/coercion'; +import { EntityAlias } from '@shared/models/alias.models'; +import { IAliasController } from '@core/api/widget-api.models'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { MatAutocomplete, MatAutocompleteSelectedEvent, MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { EntityAliasSelectCallbacks } from './entity-alias-select.component.models'; +import {ENTER} from '@angular/cdk/keycodes'; +import { ErrorStateMatcher } from '@angular/material/core'; + +@Component({ + selector: 'tb-entity-alias-select', + templateUrl: './entity-alias-select.component.html', + styleUrls: ['./entity-alias-select.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAliasSelectComponent), + multi: true + }, + { + provide: ErrorStateMatcher, + useExisting: EntityAliasSelectComponent + }] +}) +export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher { + + selectEntityAliasFormGroup: FormGroup; + + modelValue: string | null; + + @Input() + aliasController: IAliasController; + + @Input() + allowedEntityTypes: Array; + + @Input() + callbacks: EntityAliasSelectCallbacks; + + @ViewChild('entityAliasAutocomplete', {static: false}) entityAliasAutocomplete: MatAutocomplete; + @ViewChild('autocomplete', {static: false, read: MatAutocompleteTrigger}) autoCompleteTrigger: MatAutocompleteTrigger; + + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('entityAliasInput', {static: true}) entityAliasInput: ElementRef; + + entityAliasList: Array = []; + + filteredEntityAliases: Observable>; + + private searchText = ''; + + private dirty = false; + + private creatingEntityAlias = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private entityService: EntityService, + public translate: TranslateService, + public truncate: TruncatePipe, + private fb: FormBuilder) { + this.selectEntityAliasFormGroup = this.fb.group({ + entityAlias: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + const entityAliases = this.aliasController.getEntityAliases(); + for (const aliasId of Object.keys(entityAliases)) { + if (this.allowedEntityTypes && this.allowedEntityTypes.length) { + if (!this.entityService.filterAliasByEntityTypes(entityAliases[aliasId], this.allowedEntityTypes)) { + continue; + } + } + this.entityAliasList.push(entityAliases[aliasId]); + } + + this.filteredEntityAliases = this.selectEntityAliasFormGroup.get('entityAlias').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), + map(value => value ? (typeof value === 'string' ? value : value.alias) : ''), + mergeMap(name => this.fetchEntityAliases(name) ), + share() + ); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = this.required && !this.modelValue; + return originalErrorState || customErrorState; + } + + ngAfterViewInit(): void {} + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectEntityAliasFormGroup.disable({emitEvent: false}); + } else { + this.selectEntityAliasFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string | null): void { + this.searchText = ''; + let entityAlias = null; + if (value != null) { + const entityAliases = this.aliasController.getEntityAliases(); + if (entityAliases[value]) { + entityAlias = entityAliases[value]; + } + } + if (entityAlias != null) { + this.modelValue = entityAlias.id; + this.selectEntityAliasFormGroup.get('entityAlias').patchValue(entityAlias, {emitEvent: false}); + } else { + this.modelValue = null; + this.selectEntityAliasFormGroup.get('entityAlias').patchValue('', {emitEvent: false}); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectEntityAliasFormGroup.get('entityAlias').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + updateView(value: EntityAlias | null) { + const aliasId = value ? value.id : null; + if (this.modelValue !== aliasId) { + this.modelValue = aliasId; + this.propagateChange(this.modelValue); + } + } + + displayEntityAliasFn(entityAlias?: EntityAlias): string | undefined { + return entityAlias ? entityAlias.alias : undefined; + } + + fetchEntityAliases(searchText?: string): Observable> { + this.searchText = searchText; + let result = this.entityAliasList; + if (searchText && searchText.length) { + result = this.entityAliasList.filter((entityAlias) => entityAlias.alias.toLowerCase().includes(searchText.toLowerCase())); + } + return of(result); + } + + clear(value: string = '') { + this.entityAliasInput.nativeElement.value = value; + this.selectEntityAliasFormGroup.get('entityAlias').patchValue(value, {emitEvent: true}); + setTimeout(() => { + this.entityAliasInput.nativeElement.blur(); + this.entityAliasInput.nativeElement.focus(); + }, 0); + } + + textIsNotEmpty(text: string): boolean { + return (text && text != null && text.length > 0) ? true : false; + } + + entityAliasEnter($event: KeyboardEvent) { + if ($event.keyCode === ENTER) { + $event.preventDefault(); + if (!this.modelValue) { + this.createEntityAlias($event, this.searchText); + } + } + } + + createEntityAlias($event: Event, alias: string) { + $event.preventDefault(); + this.creatingEntityAlias = true; + if (this.callbacks && this.callbacks.createEntityAlias) { + this.callbacks.createEntityAlias(alias, this.allowedEntityTypes).subscribe((newAlias) => { + if (!newAlias) { + setTimeout(() => { + this.entityAliasInput.nativeElement.blur(); + this.entityAliasInput.nativeElement.focus(); + }, 0); + } else { + this.entityAliasList.push(newAlias); + this.modelValue = newAlias.id; + this.selectEntityAliasFormGroup.get('entityAlias').patchValue(newAlias, {emitEvent: true}); + this.propagateChange(this.modelValue); + } + } + ); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts index 8810495038..305b35e1f4 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts @@ -178,6 +178,7 @@ export class EntityAliasesDialogComponent extends DialogComponent DataKey; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 01b63e8fd5..f19d02da2d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -92,9 +92,44 @@ formArrayName="datasources" *ngFor="let datasourceControl of dataSettings.get('datasources').controls; let $index = index"> {{$index + 1}}. -
-
TODO: datasource: {{ datasourceControl.value | json }}
+ + + + {{ datasourceTypesTranslations.get(datasourceType) | translate }} + + + +
+ + + + + + + + + + +
+