Browse Source

Implement Alias controller. Datasources implementation.

pull/2150/head
Igor Kulikov 7 years ago
parent
commit
b9c27ed52b
  1. 285
      ui-ngx/src/app/core/api/alias-controller.ts
  2. 6
      ui-ngx/src/app/core/api/datasource-subcription.ts
  3. 23
      ui-ngx/src/app/core/api/widget-api.models.ts
  4. 18
      ui-ngx/src/app/core/api/widget-subscription.ts
  5. 73
      ui-ngx/src/app/core/http/entity.service.ts
  6. 58
      ui-ngx/src/app/core/services/utils.service.ts
  7. 2
      ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts
  8. 60
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html
  9. 23
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.models.ts
  10. 24
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss
  11. 258
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts
  12. 2
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts
  13. 7
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  14. 22
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts
  15. 49
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  16. 20
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts
  17. 47
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
  18. 153
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  19. 27
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  20. 2
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts
  21. 1
      ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.html
  22. 38
      ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.ts
  23. 2
      ui-ngx/src/app/shared/models/alias.models.ts
  24. 13
      ui-ngx/src/app/shared/models/widget.models.ts
  25. 42
      ui-ngx/src/app/shared/pipe/truncate.pipe.ts
  26. 6
      ui-ngx/src/app/shared/shared.module.ts
  27. 11
      ui-ngx/src/styles.scss

285
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<Datasource> {
return of(deepClone(alarmSource));
}
resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> {
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<AliasInfo>} = {};
[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<AliasInfo> {
return of(null);
updateEntityAliases(newEntityAliases: EntityAliases) {
const changedAliasIds: Array<string> = [];
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<Datasource>): Observable<Array<Datasource>> {
return of(deepClone(datasources));
dashboardStateChanged() {
const changedAliasIds: Array<string> = [];
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<AliasInfo> {
const aliasInfo = this.resolvedAliases[aliasId];
if (aliasInfo) {
return of(aliasInfo);
} else if (this.resolvedAliasesObservable[aliasId]) {
return this.resolvedAliasesObservable[aliasId];
} else {
const resolvedAliasSubject = new ReplaySubject<AliasInfo>();
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<Array<Datasource>> {
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<Datasource> = [];
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<Datasource> {
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<Datasource>): Observable<Array<Datasource>> {
const newDatasources = deepClone(datasources);
const observables = new Array<Observable<Array<Datasource>>>();
newDatasources.forEach((datasource) => {
observables.push(this.resolveDatasource(datasource));
});
return forkJoin(observables).pipe(
map((arrayOfDatasources) => {
const result = new Array<Datasource>();
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() {
}
}

6
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);
}
}
}

23
ui-ngx/src/app/core/api/widget-api.models.ts

@ -73,22 +73,27 @@ export interface AliasInfo {
currentEntity?: EntityInfo;
selectedId?: string;
resolvedEntities?: Array<EntityInfo>;
[key: string]: any | null;
// TODO:
entityParamName?: string;
resolveMultiple?: boolean;
}
export interface StateEntityInfo {
entityParamName: string;
entityId: EntityId;
}
export interface IAliasController {
entityAliasesChanged: Observable<Array<string>>;
entityAliasResolved: Observable<string>;
getAliasInfo(aliasId: string): Observable<AliasInfo>;
getEntityAliasId(aliasName: string): string;
getInstantAliasInfo(aliasId: string): AliasInfo;
resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>>;
resolveAlarmSource(alarmSource: Datasource): Observable<Datasource>;
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<KeyInfo>;
functions?: Array<KeyInfo>;
alarmFields?: Array<KeyInfo>;
[key: string]: any;
deviceId?: string;
deviceName?: string;
deviceNamePrefix?: string;
deviceIds?: Array<string>;
}
export interface WidgetSubscriptionContext {
@ -178,8 +189,6 @@ export interface WidgetSubscriptionOptions {
decimals?: number;
units?: string;
callbacks?: WidgetSubscriptionCallbacks;
[key: string]: any;
// TODO:
}
export interface SubscriptionEntityInfo {

18
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();
}
}
}

73
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<EntityType | AliasEntityType>): 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<EntityType | AliasEntityType>): boolean {
if (!entityTypes || !entityTypes.length) {
@ -512,6 +561,26 @@ export class EntityService {
}
}
public resolveAlias(entityAlias: EntityAlias, stateParams: StateParams): Observable<AliasInfo> {
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<EntityAliasFilterResult> {
const result: EntityAliasFilterResult = {

58
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<string> = [
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<DataKey> = [];
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<DataKey> {
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<Datasource>) {
let index = 0;
datasources.forEach((datasource) => {

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

@ -86,7 +86,7 @@ export class EntityAliasDialogComponent extends DialogComponent<EntityAliasDialo
this.entityAliases.push(data.entityAliases[aliasId]);
}
}
if (this.isAdd) {
if (this.isAdd && !this.data.alias) {
this.alias = {
id: null,
alias: '',

60
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html

@ -0,0 +1,60 @@
<!--
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 floatLabel="always" hideRequiredMarker [formGroup]="selectEntityAliasFormGroup" class="mat-block">
<mat-label></mat-label>
<input matInput type="text" placeholder="{{ 'entity.entity-alias' | translate }}"
#entityAliasInput
formControlName="entityAlias"
(focusin)="onFocus()"
[required]="required"
(keydown)="entityAliasEnter($event)"
(keypress)="entityAliasEnter($event)"
[matAutocomplete]="entityAliasAutocomplete">
<button *ngIf="selectEntityAliasFormGroup.get('entityAlias').value && !disabled"
type="button"
matSuffix mat-button mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete class="tb-autocomplete"
#entityAliasAutocomplete="matAutocomplete"
[displayWith]="displayEntityAliasFn">
<mat-option *ngFor="let entityAlias of filteredEntityAliases | async" [value]="entityAlias">
<span [innerHTML]="entityAlias.alias | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredEntityAliases | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
<span translate>entity.no-aliases-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('entity.no-alias-matching',
{alias: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span>
</ng-template>
<span>
<a translate (click)="createEntityAlias($event, searchText)">entity.create-new-alias</a>
</span>
</div>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="!modelValue && required">
{{ 'entity.alias-required' | translate }}
</mat-error>
</mat-form-field>

23
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<EntityType>) => Observable<EntityAlias>;
}

24
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;
}
}

258
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<EntityType>;
@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<EntityAlias> = [];
filteredEntityAliases: Observable<Array<EntityAlias>>;
private searchText = '';
private dirty = false;
private creatingEntityAlias = false;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
@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<Array<EntityAlias>> {
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);
}
}
);
}
}
}

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

@ -178,6 +178,7 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
message, this.translate.instant('action.close'), true);
} else {
(this.entityAliasesFormGroup.get('entityAliases') as FormArray).removeAt(index);
this.entityAliasesFormGroup.markAsDirty();
}
}
@ -217,6 +218,7 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
aliasFormControl.get('filter').patchValue(entityAlias.filter);
aliasFormControl.get('resolveMultiple').patchValue(entityAlias.filter.resolveMultiple);
}
this.entityAliasesFormGroup.markAsDirty();
}
});
}

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

@ -46,6 +46,7 @@ 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';
import { EntityAliasSelectComponent } from './alias/entity-alias-select.component';
@NgModule({
entryComponents: [
@ -90,7 +91,8 @@ import { RelationFiltersComponent } from './relation/relation-filters.component'
LegendComponent,
WidgetConfigComponent,
EntityFilterViewComponent,
EntityFilterComponent
EntityFilterComponent,
EntityAliasSelectComponent
],
imports: [
CommonModule,
@ -117,7 +119,8 @@ import { RelationFiltersComponent } from './relation/relation-filters.component'
LegendComponent,
WidgetConfigComponent,
EntityFilterViewComponent,
EntityFilterComponent
EntityFilterComponent,
EntityAliasSelectComponent
],
providers: [
WidgetComponentService

22
ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts

@ -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.
///
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DataKey } from '@shared/models/widget.models';
export interface DataKeysCallbacks {
generateDataKey: (chip: any, type: DataKeyType) => DataKey;
}

49
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">
<span fxFlex="5">{{$index + 1}}.</span>
<div class="mat-elevation-z4" fxFlex fxLayout="row" fxLayoutAlign="start center"
<div [formGroupName]="$index" 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>
<mat-form-field class="tb-datasource-type">
<mat-select matInput formControlName="type">
<mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
{{ datasourceTypesTranslations.get(datasourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<section fxFlex class="tb-datasource" [ngSwitch]="datasourceControl.get('type').value">
<ng-template [ngSwitchCase]="datasourceType.function">
<mat-form-field floatLabel="always" [fxShow]="widgetType !== widgetTypes.alarm"
class="tb-datasource-name" style="min-width: 200px;">
<mat-label></mat-label>
<input matInput
placeholder="{{ 'datasource.name' | translate }}"
formControlName="name">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="datasourceType.entity">
<tb-entity-alias-select
[required]="datasourceControl.get('type').value === datasourceType.entity"
[aliasController]="aliasController"
formControlName="entityAliasId"
[callbacks]="widgetConfigCallbacks">
</tb-entity-alias-select>
</ng-template>
</section>
<!--tb-datasource fxFlex
[widgetType]="widgetType"
[maxDataKeys]="typeParameters?.maxDataKeys"
[optDataKeys]="typeParameters?.dataKeysOptional"
[aliasController]="aliasController"
[functionsOnly]="functionsOnly"
[datakeySettingsSchema]="dataKeySettingsSchema"
[callbacks]="widgetConfigCallbacks"
[formControl]="datasourceControl"
></tb-datasource-->
<button [disabled]="isLoading$ | async"
mat-button mat-icon-button color="primary"
style="min-width: 40px;"
@ -127,8 +162,14 @@
{{ 'widget-config.target-device' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<div style="padding: 0 5px;">
<div [formGroup]="targetDeviceSettings" style="padding: 0 5px;">
<tb-entity-alias-select fxFlex
[required]="!widgetEditMode"
[aliasController]="aliasController"
[allowedEntityTypes]="[entityTypes.DEVICE]"
[callbacks]="widgetConfigCallbacks"
formControlName="targetDeviceAliasId">
</tb-entity-alias-select>
</div>
</mat-expansion-panel>
</div>

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

@ -0,0 +1,20 @@
///
/// 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 { EntityAliasSelectCallbacks } from '../alias/entity-alias-select.component.models';
import { DataKeysCallbacks } from './data-keys.component.models';
export type WidgetConfigCallbacks = EntityAliasSelectCallbacks & DataKeysCallbacks;

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

@ -13,11 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../scss/constants';
:host {
.tb-widget-config {
.tb-advanced-widget-config {
height: 100%;
}
.tb-datasource-type {
min-width: 110px;
}
.tb-datasource {
@media #{$mat-gt-sm} {
padding-left: 8px;
}
}
.tb-datasource-name {
@media #{$mat-gt-sm} {
width: 200px;
max-width: 200px;
}
}
}
}
@ -28,8 +45,34 @@
height: 100%;
}
}
.mat-expansion-panel.tb-datasources.mat-expanded {
overflow: visible;
.mat-expansion-panel {
&.tb-datasources.mat-expanded {
overflow: visible;
}
.mat-expansion-panel-content {
font: inherit;
}
}
.tb-datasource-type {
.mat-form-field-infix {
width: 100%;
}
.mat-select-value {
min-width: 92px;
}
}
.tb-datasource {
tb-entity-alias-select {
.mat-form-field {
margin-top: 13px;
}
@media #{$mat-gt-sm} {
.mat-form-field {
width: 200px;
max-width: 200px;
}
}
}
}
}
}

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

@ -21,7 +21,7 @@ import { AppState } from '@core/core.state';
import {
DataKey,
Datasource,
DatasourceType,
DatasourceType, datasourceTypeTranslationMap,
LegendConfig,
WidgetActionDescriptor,
WidgetActionSource,
@ -30,6 +30,7 @@ import {
WidgetTypeParameters
} from '@shared/models/widget.models';
import {
AbstractControl,
ControlValueAccessor,
FormArray,
FormBuilder,
@ -44,10 +45,19 @@ import { WidgetConfigComponentData } from '@home/models/widget-component.models'
import { deepClone, isDefined, isObject } from '@app/core/utils';
import { alarmFields, AlarmSearchStatus } from '@shared/models/alarm.models';
import { IAliasController } from '@core/api/widget-api.models';
import { EntityAlias } from '@shared/models/alias.models';
import { EntityAlias, EntityAliases } 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';
import { EntityType } from '@shared/models/entity-type.models';
import { Observable, of, Subscription } from 'rxjs';
import { WidgetConfigCallbacks } from '@home/components/widget/widget-config.component.models';
import {
EntityAliasDialogComponent,
EntityAliasDialogData
} from '@home/components/alias/entity-alias-dialog.component';
import { tap } from 'rxjs/operators';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'tb-widget-config',
@ -70,6 +80,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
widgetTypes = widgetType;
entityTypes = EntityType;
alarmSearchStatuses = Object.keys(AlarmSearchStatus);
@Input()
@ -87,6 +99,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
@Input()
aliasController: IAliasController;
@Input()
entityAliases: EntityAliases;
@Input()
widgetSettingsSchema: any;
@ -100,6 +115,17 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
widgetType: widgetType;
datasourceType = DatasourceType;
datasourceTypes: Array<DatasourceType>;
datasourceTypesTranslations = datasourceTypeTranslationMap;
widgetConfigCallbacks: WidgetConfigCallbacks = {
createEntityAlias: this.createEntityAlias.bind(this),
generateDataKey: this.generateDataKey.bind(this)
};
widgetEditMode = this.utils.widgetEditMode;
selectedTab: number;
title: string;
showTitleIcon: boolean;
@ -120,7 +146,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
showLegend: boolean;
legendConfig: LegendConfig;
actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
targetDeviceAlias: EntityAlias;
alarmSource: Datasource;
settings: WidgetConfigSettings;
mobileOrder: number;
@ -149,20 +174,50 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
private propagateChange = null;
public dataSettings: FormGroup;
public targetDeviceSettings: FormGroup;
private dataSettingsChangesSubscription: Subscription;
private targetDeviceSettingsSubscription: Subscription;
constructor(protected store: Store<AppState>,
private utils: UtilsService,
private dialog: MatDialog,
private translate: TranslateService,
private fb: FormBuilder) {
super(store);
}
ngOnInit(): void {
if (this.functionsOnly) {
this.datasourceTypes = [DatasourceType.function];
} else {
this.datasourceTypes = [DatasourceType.function, DatasourceType.entity];
}
}
private removeChangeSubscriptions() {
if (this.dataSettingsChangesSubscription) {
this.dataSettingsChangesSubscription.unsubscribe();
this.dataSettingsChangesSubscription = null;
}
if (this.targetDeviceSettingsSubscription) {
this.targetDeviceSettingsSubscription.unsubscribe();
this.targetDeviceSettingsSubscription = null;
}
}
private createChangeSubscriptions() {
this.dataSettingsChangesSubscription = this.dataSettings.valueChanges.subscribe(
() => this.updateDataSettings()
);
this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe(
() => this.updateTargetDeviceSettings()
);
}
private buildForms() {
this.dataSettings = this.fb.group({});
this.targetDeviceSettings = 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));
@ -188,11 +243,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.widgetType !== widgetType.static) {
this.dataSettings.addControl('datasources',
this.fb.array([]));
} else if (this.widgetType === widgetType.rpc) {
this.targetDeviceSettings.addControl('targetDeviceAliasId',
this.fb.control(null,
this.widgetEditMode ? [] : [Validators.required]));
}
}
this.dataSettings.valueChanges.subscribe(
() => this.updateModel()
);
}
registerOnChange(fn: any): void {
@ -208,6 +264,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
writeValue(value: WidgetConfigComponentData): void {
this.modelValue = value;
this.removeChangeSubscriptions();
if (this.modelValue) {
if (this.widgetType !== this.modelValue.widgetType) {
this.widgetType = this.modelValue.widgetType;
@ -268,25 +325,28 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.widgetType !== widgetType.alarm &&
this.widgetType !== widgetType.static) {
const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray;
datasourcesFormArray.controls.length = 0;
datasourcesFormArray.clear();
if (config.datasources) {
config.datasources.forEach((datasource) => {
datasourcesFormArray.controls.push(this.fb.control(datasource));
datasourcesFormArray.push(this.buildDatasourceForm(datasource));
});
datasourcesFormArray.setValue(config.datasources, {emitEvent: false});
}
} else if (this.widgetType === widgetType.rpc) {
let targetDeviceAliasId: string;
if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) {
const aliasId = config.targetDeviceAliasIds[0];
const entityAliases = this.aliasController.getEntityAliases();
if (entityAliases[aliasId]) {
this.targetDeviceAlias = entityAliases[aliasId];
targetDeviceAliasId = entityAliases[aliasId].id;
} else {
this.targetDeviceAlias = null;
targetDeviceAliasId = null;
}
} else {
this.targetDeviceAlias = null;
targetDeviceAliasId = null;
}
this.targetDeviceSettings.patchValue({
targetDeviceAliasId
}, {emitEvent: false});
} else if (this.widgetType === widgetType.alarm) {
this.dataSettings.patchValue(
{ alarmSearchStatus: isDefined(config.alarmSearchStatus) ?
@ -315,9 +375,38 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.mobileHeight = undefined;
}
}
this.createChangeSubscriptions();
}
}
private buildDatasourceForm(datasource?: Datasource): AbstractControl {
const datasourceFormGroup = this.fb.group(
{
type: [datasource ? datasource.type : null, [Validators.required]],
name: [datasource ? datasource.name : null, []],
entityAliasId: [datasource ? datasource.entityAliasId : null,
datasource && datasource.type === DatasourceType.entity ? [Validators.required] : []],
dataKeys: [datasource ? datasource.name : null, []]
}
);
datasourceFormGroup.get('type').valueChanges.subscribe((type: DatasourceType) => {
let dataKeys;
if (this.widgetType === widgetType.alarm) {
dataKeys = this.utils.getDefaultAlarmDataKeys();
} else {
dataKeys = [];
}
datasourceFormGroup.patchValue({
dataKeys
});
datasourceFormGroup.get('entityAliasId').setValidators(
type === DatasourceType.entity ? [Validators.required] : []
);
datasourceFormGroup.get('entityAliasId').updateValueAndValidity();
});
return datasourceFormGroup;
}
private updateSchemaForm() {
if (this.widgetSettingsSchema && this.widgetSettingsSchema.schema) {
this.currentSettingsSchema = this.widgetSettingsSchema.schema;
@ -332,7 +421,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
}
private updateModel() {
private updateDataSettings() {
if (this.modelValue) {
if (this.modelValue.config) {
Object.assign(this.modelValue.config, this.dataSettings.value);
@ -341,6 +430,20 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
}
private updateTargetDeviceSettings() {
if (this.modelValue) {
if (this.modelValue.config) {
const targetDeviceAliasId: string = this.targetDeviceSettings.get('targetDeviceAliasId').value;
if (targetDeviceAliasId) {
this.modelValue.config.targetDeviceAliasIds = [targetDeviceAliasId];
} else {
this.modelValue.config.targetDeviceAliasIds = [];
}
}
this.propagateChange(this.modelValue);
}
}
public displayAdvanced(): boolean {
return this.widgetSettingsSchema && this.widgetSettingsSchema.schema;
}
@ -360,7 +463,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
};
}
const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray;
datasourcesFormArray.push(this.fb.control(newDatasource));
datasourcesFormArray.push(this.buildDatasourceForm(newDatasource));
}
public generateDataKey(chip: any, type: DataKeyType): DataKey {
@ -435,6 +538,28 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
return this.utils.getMaterialColor(i);
}
private createEntityAlias(alias: string, allowedEntityTypes: Array<EntityType>): Observable<EntityAlias> {
const singleEntityAlias: EntityAlias = {id: null, alias, filter: {resolveMultiple: false}};
return this.dialog.open<EntityAliasDialogComponent, EntityAliasDialogData,
EntityAlias>(EntityAliasDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd: true,
allowedEntityTypes,
entityAliases: this.entityAliases,
alias: singleEntityAlias
}
}).afterClosed().pipe(
tap((entityAlias) => {
if (entityAlias) {
this.entityAliases[entityAlias.id] = entityAlias;
this.aliasController.updateEntityAliases(this.entityAliases);
}
})
);
}
public validate(c: FormControl) {
if (!this.dataSettings.valid) {
return {

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

@ -459,6 +459,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.initialize().subscribe(
() => {
this.onInit();
},
(err) => {
// console.log(err);
}
);
}
@ -606,9 +609,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
initSubject.next();
initSubject.complete();
},
() => {
(err) => {
this.subscriptionInited = true;
initSubject.error(null);
initSubject.error(err);
}
);
} else {
@ -675,8 +678,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
createSubscriptionSubject.next(subscription);
createSubscriptionSubject.complete();
},
() => {
createSubscriptionSubject.error(null);
(err) => {
createSubscriptionSubject.error(err);
}
);
return createSubscriptionSubject.asObservable();
@ -716,13 +719,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
createSubscriptionSubject.next(subscription);
createSubscriptionSubject.complete();
},
() => {
createSubscriptionSubject.error(null);
(err) => {
createSubscriptionSubject.error(err);
}
);
},
() => {
createSubscriptionSubject.error(null);
(err) => {
createSubscriptionSubject.error(err);
}
);
return createSubscriptionSubject.asObservable();
@ -806,8 +809,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
createSubscriptionSubject.next();
createSubscriptionSubject.complete();
},
() => {
createSubscriptionSubject.error(null);
(err) => {
createSubscriptionSubject.error(err);
}
);
} else if (this.widget.type === widgetType.rpc) {
@ -842,8 +845,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
createSubscriptionSubject.next();
createSubscriptionSubject.complete();
},
() => {
createSubscriptionSubject.error(null);
(err) => {
createSubscriptionSubject.error(err);
}
);
this.cd.detectChanges();

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

@ -246,7 +246,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
this.dashboardCtx.aliasController = new AliasController(this.utils,
this.entityService,
this.dashboardCtx.stateController,
() => this.dashboardCtx.stateController,
this.dashboardConfiguration.entityAliases);
if (this.widgetEditMode) {

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

@ -25,6 +25,7 @@
[dataKeySettingsSchema]="dataKeySettingsSchema"
[aliasController]="aliasController"
[functionsOnly]="widgetEditMode"
[entityAliases]="dashboard.configuration.entityAliases"
formControlName="widgetConfig">
</tb-widget-config>
</fieldset>

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

@ -28,8 +28,21 @@ import { IAliasController } from '@core/api/widget-api.models';
import { Widget, WidgetActionSource, WidgetTypeParameters } from '@shared/models/widget.models';
import { WidgetComponentService } from '@home/components/widget/widget-component.service';
import { WidgetConfigComponentData } from '../../models/widget-component.models';
import { isDefined, isString } from '@core/utils';
import { deepClone, isDefined, isString } from '@core/utils';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { EntityType } from '@shared/models/entity-type.models';
import { Observable, of } from 'rxjs';
import { EntityAlias, EntityAliases } from '@shared/models/alias.models';
import { WidgetConfigCallbacks } from '@home/components/widget/widget-config.component.models';
import {
EntityAliasesDialogComponent,
EntityAliasesDialogData
} from '@home/components/alias/entity-aliases-dialog.component';
import {
EntityAliasDialogComponent,
EntityAliasDialogData
} from '@home/components/alias/entity-alias-dialog.component';
import { tap } from 'rxjs/operators';
@Component({
selector: 'tb-edit-widget',
@ -65,6 +78,7 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
functionsOnly: boolean;
constructor(protected store: Store<AppState>,
private dialog: MatDialog,
private widgetComponentService: WidgetComponentService) {
super(store);
}
@ -113,4 +127,26 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
this.functionsOnly = this.dashboard ? false : true;
this.widgetFormGroup.reset({widgetConfig: this.widgetConfig});
}
private createEntityAlias(alias: string, allowedEntityTypes: Array<EntityType>): Observable<EntityAlias> {
const singleEntityAlias: EntityAlias = {id: null, alias, filter: {resolveMultiple: false}};
return this.dialog.open<EntityAliasDialogComponent, EntityAliasDialogData,
EntityAlias>(EntityAliasDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd: true,
allowedEntityTypes,
entityAliases: this.dashboard.configuration.entityAliases,
alias: singleEntityAlias
}
}).afterClosed().pipe(
tap((entityAlias) => {
if (entityAlias) {
this.dashboard.configuration.entityAliases[entityAlias.id] = entityAlias;
this.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases);
}
})
);
}
}

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

@ -129,7 +129,7 @@ export type EntityFilters =
export interface EntityAliasFilter extends EntityFilters {
type?: AliasFilterType;
resolveMultiple: boolean;
resolveMultiple?: boolean;
}
export interface EntityAlias {

13
ui-ngx/src/app/shared/models/widget.models.ts

@ -22,6 +22,7 @@ import { EntityType } from '@shared/models/entity-type.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { Data } from '@angular/router';
import { DataKeyType } from './telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
export enum widgetType {
timeseries = 'timeseries',
@ -227,9 +228,17 @@ export enum DatasourceType {
entity = 'entity'
}
export const datasourceTypeTranslationMap = new Map<DatasourceType, string>(
[
[ DatasourceType.function, 'function.function' ],
[ DatasourceType.entity, 'entity.entity' ]
]
);
export interface Datasource {
type?: DatasourceType | any;
name?: string;
aliasName?: string;
dataKeys?: Array<DataKey>;
entityType?: EntityType;
entityId?: string;
@ -237,6 +246,10 @@ export interface Datasource {
entityAliasId?: string;
unresolvedStateEntity?: boolean;
dataReceived?: boolean;
entity?: BaseData<EntityId>;
entityLabel?: string;
entityDescription?: string;
generated?: boolean;
[key: string]: any;
// TODO:
}

42
ui-ngx/src/app/shared/pipe/truncate.pipe.ts

@ -0,0 +1,42 @@
///
/// 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 {Pipe, PipeTransform} from '@angular/core';
import { isString } from '@core/utils';
@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
transform(text: string, wordwise: boolean, max: any, tail: string): string {
if (!text) { return ''; }
if (isString(max)) {
max = parseInt(max, 10);
}
if (!max) { return text; }
if (text.length <= max) { return text; }
text = text.substr(0, max);
if (wordwise) {
let lastspace = text.lastIndexOf(' ');
if (lastspace !== -1) {
if (text.charAt(lastspace - 1) === '.' || text.charAt(lastspace - 1) === ',') {
lastspace = lastspace - 1;
}
text = text.substr(0, lastspace);
}
}
return text + (tail || ' …');
}
}

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

@ -99,13 +99,15 @@ 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';
import { TruncatePipe } from './pipe/truncate.pipe';
@NgModule({
providers: [
DatePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
HighlightPipe
HighlightPipe,
TruncatePipe
],
entryComponents: [
TbSnackBarComponent,
@ -158,6 +160,7 @@ import { EntitySubTypeListComponent } from './components/entity/entity-subtype-l
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
HighlightPipe,
TruncatePipe,
KeyboardShortcutPipe
],
imports: [
@ -283,6 +286,7 @@ import { EntitySubTypeListComponent } from './components/entity/entity-subtype-l
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
HighlightPipe,
TruncatePipe,
KeyboardShortcutPipe,
TranslateModule
]

11
ui-ngx/src/styles.scss

@ -389,4 +389,15 @@ mat-label {
white-space: normal !important;
}
}
.mat-option.tb-not-found {
padding: 0;
border-bottom: none;
.mat-option-text {
display: block;
.tb-not-found-content {
padding: 8px 16px 7px;
border-bottom: 1px solid #eee;
}
}
}
}

Loading…
Cancel
Save