Browse Source

UI: Entities data query

pull/3053/head
Igor Kulikov 6 years ago
parent
commit
f37ebb66aa
  1. 32
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  3. 1
      dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java
  4. 54
      ui-ngx/src/app/core/api/alias-controller.ts
  5. 361
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  6. 91
      ui-ngx/src/app/core/api/entity-data.service.ts
  7. 13
      ui-ngx/src/app/core/api/widget-api.models.ts
  8. 215
      ui-ngx/src/app/core/api/widget-subscription.ts
  9. 87
      ui-ngx/src/app/core/http/entity.service.ts
  10. 11
      ui-ngx/src/app/core/ws/telemetry-websocket.service.ts
  11. 2
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html
  12. 156
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  13. 58
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  14. 7
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  15. 9
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  16. 31
      ui-ngx/src/app/shared/models/query/query.models.ts
  17. 11
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  18. 2
      ui-ngx/src/app/shared/models/widget.models.ts

32
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

2
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java

@ -452,7 +452,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
} }
private String entityNameQuery(EntityNameFilter filter) { private String entityNameQuery(EntityNameFilter filter) {
return String.format("lower(e.search_text) like lower(concat(%s, '%%'))", filter.getEntityNameFilter()); return String.format("lower(e.search_text) like lower(concat('%s', '%%'))", filter.getEntityNameFilter());
} }
private String typeQuery(EntityFilter filter) { private String typeQuery(EntityFilter filter) {

1
dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java

@ -48,6 +48,7 @@ public class EntityKeyMapping {
static { static {
entityFieldColumnMap.put("createdTime", "id"); entityFieldColumnMap.put("createdTime", "id");
entityFieldColumnMap.put("entityType", "entity_type");
entityFieldColumnMap.put("name", "name"); entityFieldColumnMap.put("name", "name");
entityFieldColumnMap.put("type", "type"); entityFieldColumnMap.put("type", "type");
entityFieldColumnMap.put("label", "label"); entityFieldColumnMap.put("label", "label");

54
ui-ngx/src/app/core/api/alias-controller.ts

@ -22,8 +22,8 @@ import { EntityService } from '@core/http/entity.service';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { AliasFilterType, EntityAliases } from '@shared/models/alias.models'; import { AliasFilterType, EntityAliases } from '@shared/models/alias.models';
import { EntityInfo } from '@shared/models/entity.models'; import { EntityInfo } from '@shared/models/entity.models';
import { map } from 'rxjs/operators'; import { map, mergeMap } from 'rxjs/operators';
import { defaultEntityDataPageLink } from '@shared/models/query/query.models'; import { createDefaultEntityDataPageLink, defaultEntityDataPageLink } from '@shared/models/query/query.models';
export class AliasController implements IAliasController { export class AliasController implements IAliasController {
@ -169,7 +169,24 @@ export class AliasController implements IAliasController {
} }
} }
private resolveDatasource(datasource: Datasource, isSingle?: boolean): Observable<Array<Datasource>> { resolveSingleEntityInfo(aliasId: string): Observable<EntityInfo> {
return this.getAliasInfo(aliasId).pipe(
mergeMap((aliasInfo) => {
if (aliasInfo.resolveMultiple) {
if (aliasInfo.entityFilter) {
return this.entityService.findSingleEntityInfoByEntityFilter(aliasInfo.entityFilter,
{ignoreLoading: true, ignoreErrors: true});
} else {
return of(null);
}
} else {
return of(aliasInfo.currentEntity);
}
})
);
}
private resolveDatasource(datasource: Datasource, isSingle?: boolean): Observable<Datasource> {
if (datasource.type === DatasourceType.entity) { if (datasource.type === DatasourceType.entity) {
if (datasource.entityAliasId) { if (datasource.entityAliasId) {
return this.getAliasInfo(datasource.entityAliasId).pipe( return this.getAliasInfo(datasource.entityAliasId).pipe(
@ -200,14 +217,14 @@ export class AliasController implements IAliasController {
datasources.push(newDatasource); datasources.push(newDatasource);
} }
return datasources;*/ return datasources;*/
return [newDatasource]; return newDatasource;
} else { } else {
if (aliasInfo.stateEntity) { if (aliasInfo.stateEntity) {
newDatasource = deepClone(datasource); newDatasource = deepClone(datasource);
newDatasource.unresolvedStateEntity = true; newDatasource.unresolvedStateEntity = true;
return [newDatasource]; return newDatasource;
} else { } else {
return []; return null;
// throw new Error('Unable to resolve datasource.'); // throw new Error('Unable to resolve datasource.');
} }
} }
@ -232,13 +249,13 @@ export class AliasController implements IAliasController {
entityType: entity.entityType entityType: entity.entityType
} }
}; };
return [datasource]; return datasource;
} else { } else {
if (aliasInfo.stateEntity) { if (aliasInfo.stateEntity) {
datasource.unresolvedStateEntity = true; datasource.unresolvedStateEntity = true;
return [datasource]; return datasource;
} else { } else {
return []; return null;
// throw new Error('Unable to resolve datasource.'); // throw new Error('Unable to resolve datasource.');
} }
} }
@ -248,10 +265,10 @@ export class AliasController implements IAliasController {
} else { } else {
datasource.aliasName = datasource.entityName; datasource.aliasName = datasource.entityName;
datasource.name = datasource.entityName; datasource.name = datasource.entityName;
return of([datasource]); return of(datasource);
} }
} else { } else {
return of([datasource]); return of(datasource);
} }
} }
@ -354,18 +371,14 @@ export class AliasController implements IAliasController {
); );
} }
resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> { resolveDatasources(datasources: Array<Datasource>, singleEntity?: boolean): Observable<Array<Datasource>> {
const newDatasources = deepClone(datasources); const newDatasources = deepClone(singleEntity ? [datasources[0]] : datasources);
const observables = new Array<Observable<Array<Datasource>>>(); const observables = new Array<Observable<Datasource>>();
newDatasources.forEach((datasource) => { newDatasources.forEach((datasource) => {
observables.push(this.resolveDatasource(datasource)); observables.push(this.resolveDatasource(datasource));
}); });
return forkJoin(observables).pipe( return forkJoin(observables).pipe(
map((arrayOfDatasources) => { map((result) => {
const result = new Array<Datasource>();
arrayOfDatasources.forEach((datasourcesArray) => {
result.push(...datasourcesArray);
});
let functionIndex = 0; let functionIndex = 0;
result.forEach((datasource) => { result.forEach((datasource) => {
if (datasource.type === DatasourceType.function) { if (datasource.type === DatasourceType.function) {
@ -386,6 +399,9 @@ export class AliasController implements IAliasController {
datasource.name = 'Unresolved'; datasource.name = 'Unresolved';
datasource.entityName = 'Unresolved'; datasource.entityName = 'Unresolved';
} else if (datasource.type === DatasourceType.entity) { } else if (datasource.type === DatasourceType.entity) {
if (singleEntity) {
datasource.pageLink = createDefaultEntityDataPageLink(1);
}
if (!datasource.pageLink) { if (!datasource.pageLink) {
datasource.pageLink = deepClone(defaultEntityDataPageLink); datasource.pageLink = deepClone(defaultEntityDataPageLink);
} }

361
ui-ngx/src/app/core/api/entity-data-subscription.ts

@ -35,19 +35,21 @@ import {
TelemetrySubscriber TelemetrySubscriber
} from '@shared/models/telemetry/telemetry.models'; } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { EntityDataListener } from '@core/api/entity-data.service'; import { EntityDataListener, EntityDataLoadResult } from '@core/api/entity-data.service';
import { deepClone, isDefinedAndNotNull, isObject, objectHashCode } from '@core/utils'; import { deepClone, isDefinedAndNotNull, isObject, objectHashCode } from '@core/utils';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { DataAggregator } from '@core/api/data-aggregator'; import { DataAggregator } from '@core/api/data-aggregator';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
export interface EntityDataSubscriptionOptions { export interface EntityDataSubscriptionOptions {
datasourceType: DatasourceType; datasourceType: DatasourceType;
dataKeys: Array<SubscriptionDataKey>; dataKeys: Array<SubscriptionDataKey>;
type: widgetType; type: widgetType;
entityFilter?: EntityFilter; entityFilter?: EntityFilter;
isLatestDataSubscription?: boolean;
pageLink?: EntityDataPageLink; pageLink?: EntityDataPageLink;
keyFilters?: Array<KeyFilter>; keyFilters?: Array<KeyFilter>;
subscriptionTimewindow?: SubscriptionTimewindow; subscriptionTimewindow?: SubscriptionTimewindow;
@ -59,21 +61,19 @@ declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number, dataKeyInd
export class EntityDataSubscription { export class EntityDataSubscription {
private listeners: Array<EntityDataListener> = [];
private datasourceType: DatasourceType = this.entityDataSubscriptionOptions.datasourceType; private datasourceType: DatasourceType = this.entityDataSubscriptionOptions.datasourceType;
private history: boolean;
private history = this.entityDataSubscriptionOptions.subscriptionTimewindow && private realtime: boolean;
isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
private realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs);
private subscriber: TelemetrySubscriber; private subscriber: TelemetrySubscriber;
private dataCommand: EntityDataCmd;
private subsCommand: EntityDataCmd;
private attrFields: Array<EntityKey>; private attrFields: Array<EntityKey>;
private tsFields: Array<EntityKey>; private tsFields: Array<EntityKey>;
private latestValues: Array<EntityKey>; private latestValues: Array<EntityKey>;
private entityDataResolveSubject: Subject<EntityDataLoadResult>;
private pageData: PageData<EntityData>; private pageData: PageData<EntityData>;
private subsTw: SubscriptionTimewindow; private subsTw: SubscriptionTimewindow;
private dataAggregators: Array<DataAggregator>; private dataAggregators: Array<DataAggregator>;
@ -87,7 +87,11 @@ export class EntityDataSubscription {
private tickElapsed = 0; private tickElapsed = 0;
private timer: Timeout; private timer: Timeout;
constructor(private entityDataSubscriptionOptions: EntityDataSubscriptionOptions, private dataResolved = false;
private started = false;
constructor(public entityDataSubscriptionOptions: EntityDataSubscriptionOptions,
private listener: EntityDataListener,
private telemetryService: TelemetryService, private telemetryService: TelemetryService,
private utils: UtilsService) { private utils: UtilsService) {
this.initializeSubscription(); this.initializeSubscription();
@ -126,50 +130,6 @@ export class EntityDataSubscription {
} }
dataKey.key = key; dataKey.key = key;
} }
if (this.datasourceType === DatasourceType.function) {
this.frequency = 1000;
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
this.frequency = Math.min(this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.interval, 5000);
}
}
}
public addListener(listener: EntityDataListener) {
this.listeners.push(listener);
}
public hasListeners(): boolean {
return this.listeners.length > 0;
}
public removeListener(listener: EntityDataListener) {
this.listeners.splice(this.listeners.indexOf(listener), 1);
}
public syncListener(listener: EntityDataListener) {
if (this.pageData) {
let key: string;
let dataKey: SubscriptionDataKey;
const data: Array<Array<DataSetHolder>> = [];
for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
data[dataIndex] = [];
for (key of Object.keys(this.dataKeys)) {
if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
for (let i = 0; i < dataKeysList.length; i++) {
dataKey = dataKeysList[i];
const datasourceKey = `${key}_${i}`;
data[dataIndex][dataKey.index] = this.datasourceData[dataIndex][datasourceKey];
}
} else {
dataKey = this.dataKeys[key] as SubscriptionDataKey;
data[dataIndex][dataKey.index] = this.datasourceData[dataIndex][key];
}
}
}
listener.dataLoaded(this.pageData, data, listener.configDatasourceIndex);
}
this.listeners.push(listener);
} }
public unsubscribe() { public unsubscribe() {
@ -192,19 +152,30 @@ export class EntityDataSubscription {
this.pageData = null; this.pageData = null;
} }
public start() { public subscribe(): Observable<EntityDataLoadResult> {
this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; if (!this.entityDataSubscriptionOptions.isLatestDataSubscription) {
this.entityDataResolveSubject = new ReplaySubject(1);
} else {
this.started = true;
this.dataResolved = true;
}
if (this.datasourceType === DatasourceType.entity) { if (this.datasourceType === DatasourceType.entity) {
const entityFields: Array<EntityKey> = const entityFields: Array<EntityKey> =
this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.entityField).map( this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.entityField).map(
dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name }) dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name })
); );
if (!entityFields.find(key => key.key === 'name')) { if (!entityFields.find(key => key.key === 'name')) {
entityFields.push({ entityFields.push({
type: EntityKeyType.ENTITY_FIELD, type: EntityKeyType.ENTITY_FIELD,
key: 'name' key: 'name'
}); });
} }
if (!entityFields.find(key => key.key === 'label')) {
entityFields.push({
type: EntityKeyType.ENTITY_FIELD,
key: 'label'
});
}
this.attrFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.attribute).map( this.attrFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.attribute).map(
dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name }) dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name })
@ -217,9 +188,9 @@ export class EntityDataSubscription {
this.latestValues = this.attrFields.concat(this.tsFields); this.latestValues = this.attrFields.concat(this.tsFields);
this.subscriber = new TelemetrySubscriber(this.telemetryService); this.subscriber = new TelemetrySubscriber(this.telemetryService);
const command = new EntityDataCmd(); this.dataCommand = new EntityDataCmd();
command.query = { this.dataCommand.query = {
entityFilter: this.entityDataSubscriptionOptions.entityFilter, entityFilter: this.entityDataSubscriptionOptions.entityFilter,
pageLink: this.entityDataSubscriptionOptions.pageLink, pageLink: this.entityDataSubscriptionOptions.pageLink,
keyFilters: this.entityDataSubscriptionOptions.keyFilters, keyFilters: this.entityDataSubscriptionOptions.keyFilters,
@ -227,72 +198,17 @@ export class EntityDataSubscription {
latestValues: this.latestValues latestValues: this.latestValues
}; };
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.entityDataSubscriptionOptions.isLatestDataSubscription) {
if (this.tsFields.length > 0) { if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
if (this.history) { if (this.latestValues.length > 0) {
command.historyCmd = { this.dataCommand.latestCmd = {
keys: this.tsFields.map(key => key.key), keys: this.latestValues
startTs: this.subsTw.fixedWindow.startTimeMs,
endTs: this.subsTw.fixedWindow.endTimeMs,
interval: this.subsTw.aggregation.interval,
limit: this.subsTw.aggregation.limit,
agg: this.subsTw.aggregation.type
}; };
if (this.subsTw.aggregation.stateData) {
command.historyCmd.startTs -= YEAR;
}
} else {
command.tsCmd = {
keys: this.tsFields.map(key => key.key),
startTs: this.subsTw.startTs,
timeWindow: this.subsTw.aggregation.timeWindow,
interval: this.subsTw.aggregation.interval,
limit: this.subsTw.aggregation.limit,
agg: this.subsTw.aggregation.type
}
if (this.subsTw.aggregation.stateData) {
command.historyCmd = {
keys: this.tsFields.map(key => key.key),
startTs: this.subsTw.startTs - YEAR,
endTs: this.subsTw.startTs,
interval: this.subsTw.aggregation.interval,
limit: this.subsTw.aggregation.limit,
agg: this.subsTw.aggregation.type
};
}
this.subscriber.reconnect$.subscribe(() => {
let newSubsTw: SubscriptionTimewindow = null;
this.listeners.forEach((listener) => {
if (!newSubsTw) {
newSubsTw = listener.updateRealtimeSubscription();
} else {
listener.setRealtimeSubscription(newSubsTw);
}
});
this.subsTw = newSubsTw;
command.tsCmd.startTs = this.subsTw.startTs;
command.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
command.tsCmd.interval = this.subsTw.aggregation.interval;
command.tsCmd.limit = this.subsTw.aggregation.limit;
command.tsCmd.agg = this.subsTw.aggregation.type;
if (this.subsTw.aggregation.stateData) {
command.historyCmd.startTs = this.subsTw.startTs - YEAR;
command.historyCmd.endTs = this.subsTw.startTs;
command.historyCmd.interval = this.subsTw.aggregation.interval;
command.historyCmd.limit = this.subsTw.aggregation.limit;
command.historyCmd.agg = this.subsTw.aggregation.type;
}
});
} }
} }
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
if (this.latestValues.length > 0) {
command.latestCmd = {
keys: this.latestValues.map(key => key.key)
};
}
} }
this.subscriber.subscriptionCommands.push(command);
this.subscriber.subscriptionCommands.push(this.dataCommand);
this.subscriber.entityData$.subscribe( this.subscriber.entityData$.subscribe(
(entityDataUpdate) => { (entityDataUpdate) => {
@ -304,6 +220,30 @@ export class EntityDataSubscription {
} }
); );
this.subscriber.reconnect$.subscribe(() => {
const newSubsTw: SubscriptionTimewindow = this.listener.updateRealtimeSubscription();
this.listener.setRealtimeSubscription(newSubsTw);
this.subsTw = newSubsTw;
if (this.started && !this.entityDataSubscriptionOptions.isLatestDataSubscription) {
this.subsCommand.tsCmd.startTs = this.subsTw.startTs;
this.subsCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
this.subsCommand.tsCmd.interval = this.subsTw.aggregation.interval;
this.subsCommand.tsCmd.limit = this.subsTw.aggregation.limit;
this.subsCommand.tsCmd.agg = this.subsTw.aggregation.type;
if (this.subsTw.aggregation.stateData) {
this.subsCommand.historyCmd.startTs = this.subsTw.startTs - YEAR;
this.subsCommand.historyCmd.endTs = this.subsTw.startTs;
this.subsCommand.historyCmd.interval = this.subsTw.aggregation.interval;
this.subsCommand.historyCmd.limit = this.subsTw.aggregation.limit;
this.subsCommand.historyCmd.agg = this.subsTw.aggregation.type;
}
this.subsCommand.query = this.dataCommand.query;
this.subscriber.subscriptionCommands = [this.subsCommand];
} else {
this.subscriber.subscriptionCommands = [this.dataCommand];
}
});
this.subscriber.subscribe(); this.subscriber.subscribe();
} else if (this.datasourceType === DatasourceType.function) { } else if (this.datasourceType === DatasourceType.function) {
const entityData: EntityData = { const entityData: EntityData = {
@ -325,29 +265,46 @@ export class EntityDataSubscription {
totalPages: 1 totalPages: 1
}; };
this.onPageData(pageData); this.onPageData(pageData);
this.tickScheduledTime = this.utils.currentPerfTime(); if (this.entityDataSubscriptionOptions.isLatestDataSubscription) {
if (this.history) { if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
this.onTick(true); this.frequency = 1000;
} else { this.timer = setTimeout(this.onTick.bind(this, true), 0);
this.timer = setTimeout(this.onTick.bind(this, true), 0); }
} }
} }
if (this.entityDataSubscriptionOptions.isLatestDataSubscription) {
return of(null);
} else {
return this.entityDataResolveSubject.asObservable();
}
} }
private onPageData(pageData: PageData<EntityData>) { public start() {
if (this.entityDataSubscriptionOptions.isLatestDataSubscription) {
return;
}
this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs);
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
if (this.dataAggregators) { if (this.dataAggregators) {
this.dataAggregators.forEach((aggregator) => { this.dataAggregators.forEach((aggregator) => {
aggregator.destroy(); aggregator.destroy();
}) })
this.dataAggregators = null;
} }
this.datasourceData = [];
this.dataAggregators = []; this.dataAggregators = [];
this.entityIdToDataIndex = {}; this.resetData();
let tsKeyNames;
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
let tsKeyNames = [];
if (this.datasourceType === DatasourceType.function) { if (this.datasourceType === DatasourceType.function) {
tsKeyNames = [];
for (const key of Object.keys(this.dataKeys)) { for (const key of Object.keys(this.dataKeys)) {
const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>; const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
dataKeysList.forEach((subscriptionDataKey) => { dataKeysList.forEach((subscriptionDataKey) => {
@ -357,20 +314,85 @@ export class EntityDataSubscription {
} else { } else {
tsKeyNames = this.tsFields ? this.tsFields.map(field => field.key) : []; tsKeyNames = this.tsFields ? this.tsFields.map(field => field.key) : [];
} }
} for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) {
const entityData = pageData.data[dataIndex];
this.entityIdToDataIndex[entityData.entityId.id] = dataIndex;
this.datasourceData[dataIndex] = {};
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (this.datasourceType === DatasourceType.function) { if (this.datasourceType === DatasourceType.function) {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames, this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
DataKeyType.function, dataIndex, this.notifyListeners.bind(this)); DataKeyType.function, dataIndex, this.notifyListener.bind(this));
} else if (!this.history && tsKeyNames.length) { } else if (!this.history && tsKeyNames.length) {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames, this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
DataKeyType.timeseries, dataIndex, this.notifyListeners.bind(this)); DataKeyType.timeseries, dataIndex, this.notifyListener.bind(this));
} }
} }
}
if (this.datasourceType === DatasourceType.entity) {
this.subsCommand = new EntityDataCmd();
this.subsCommand.cmdId = this.dataCommand.cmdId;
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (this.tsFields.length > 0) {
if (this.history) {
this.subsCommand.historyCmd = {
keys: this.tsFields.map(key => key.key),
startTs: this.subsTw.fixedWindow.startTimeMs,
endTs: this.subsTw.fixedWindow.endTimeMs,
interval: this.subsTw.aggregation.interval,
limit: this.subsTw.aggregation.limit,
agg: this.subsTw.aggregation.type
};
if (this.subsTw.aggregation.stateData) {
this.subsCommand.historyCmd.startTs -= YEAR;
}
} else {
this.subsCommand.tsCmd = {
keys: this.tsFields.map(key => key.key),
startTs: this.subsTw.startTs,
timeWindow: this.subsTw.aggregation.timeWindow,
interval: this.subsTw.aggregation.interval,
limit: this.subsTw.aggregation.limit,
agg: this.subsTw.aggregation.type
}
if (this.subsTw.aggregation.stateData) {
this.subsCommand.historyCmd = {
keys: this.tsFields.map(key => key.key),
startTs: this.subsTw.startTs - YEAR,
endTs: this.subsTw.startTs,
interval: this.subsTw.aggregation.interval,
limit: this.subsTw.aggregation.limit,
agg: this.subsTw.aggregation.type
};
}
}
}
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
if (this.latestValues.length > 0) {
this.subsCommand.latestCmd = {
keys: this.latestValues
};
}
}
this.subscriber.subscriptionCommands = [this.subsCommand];
this.subscriber.update();
} else if (this.datasourceType === DatasourceType.function) {
this.frequency = 1000;
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
this.frequency = Math.min(this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.interval, 5000);
}
this.tickScheduledTime = this.utils.currentPerfTime();
if (this.history) {
this.onTick(true);
} else {
this.timer = setTimeout(this.onTick.bind(this, true), 0);
}
}
this.started = true;
}
private resetData() {
this.datasourceData = [];
this.entityIdToDataIndex = {};
for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
const entityData = this.pageData.data[dataIndex];
this.entityIdToDataIndex[entityData.entityId.id] = dataIndex;
this.datasourceData[dataIndex] = {};
for (const key of Object.keys(this.dataKeys)) { for (const key of Object.keys(this.dataKeys)) {
const dataKey = this.dataKeys[key]; const dataKey = this.dataKeys[key];
if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
@ -388,7 +410,23 @@ export class EntityDataSubscription {
} }
} }
this.datasourceOrigData = deepClone(this.datasourceData); this.datasourceOrigData = deepClone(this.datasourceData);
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
for (const key of Object.keys(this.dataKeys)) {
const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>;
dataKeyList.forEach((dataKey) => {
delete dataKey.lastUpdateTime;
});
}
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
for (const key of Object.keys(this.dataKeys)) {
delete (this.dataKeys[key] as SubscriptionDataKey).lastUpdateTime;
}
}
}
private onPageData(pageData: PageData<EntityData>) {
this.pageData = pageData;
this.resetData();
const data: Array<Array<DataSetHolder>> = []; const data: Array<Array<DataSetHolder>> = [];
for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) { for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) {
const entityData = pageData.data[dataIndex]; const entityData = pageData.data[dataIndex];
@ -401,28 +439,33 @@ export class EntityDataSubscription {
} }
); );
} }
if (!this.dataResolved) {
this.pageData = pageData; this.dataResolved = true;
this.entityDataResolveSubject.next(
this.listeners.forEach((listener) => { {
listener.dataLoaded(pageData, data, pageData,
listener.configDatasourceIndex); data,
}); datasourceIndex: this.listener.configDatasourceIndex
}
);
this.entityDataResolveSubject.complete();
} else {
this.listener.dataLoaded(pageData, data,
this.listener.configDatasourceIndex);
}
} }
private onDataUpdate(update: Array<EntityData>) { private onDataUpdate(update: Array<EntityData>) {
for (const entityData of update) { for (const entityData of update) {
const dataIndex = this.entityIdToDataIndex[entityData.entityId.id]; const dataIndex = this.entityIdToDataIndex[entityData.entityId.id];
this.processEntityData(entityData, dataIndex, true, this.notifyListeners.bind(this)); this.processEntityData(entityData, dataIndex, true, this.notifyListener.bind(this));
} }
} }
private notifyListeners(data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) { private notifyListener(data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) {
this.listeners.forEach((listener) => { this.listener.dataUpdated(data,
listener.dataUpdated(data, this.listener.configDatasourceIndex,
listener.configDatasourceIndex,
dataIndex, dataKeyIndex, detectChanges); dataIndex, dataKeyIndex, detectChanges);
});
} }
private processEntityData(entityData: EntityData, dataIndex: number, aggregate: boolean, private processEntityData(entityData: EntityData, dataIndex: number, aggregate: boolean,
@ -596,14 +639,10 @@ export class EntityDataSubscription {
const value = dataKey.func(time, prevSeries[1]); const value = dataKey.func(time, prevSeries[1]);
const series: [number, any] = [time, value]; const series: [number, any] = [time, value];
this.datasourceData[0][dataKey.key].data = [series]; this.datasourceData[0][dataKey.key].data = [series];
this.listeners.forEach( this.listener.dataUpdated(this.datasourceData[0][dataKey.key],
(listener) => { this.listener.configDatasourceIndex,
listener.dataUpdated(this.datasourceData[0][dataKey.key], 0,
listener.configDatasourceIndex, dataKey.index, detectChanges);
0,
dataKey.index, detectChanges);
}
);
} }
private onTick(detectChanges: boolean) { private onTick(detectChanges: boolean) {

91
ui-ngx/src/app/core/api/entity-data.service.ts

@ -24,17 +24,24 @@ import { UtilsService } from '@core/services/utils.service';
import { SubscriptionDataKey } from '@core/api/datasource-subcription'; import { SubscriptionDataKey } from '@core/api/datasource-subcription';
import { deepClone, objectHashCode } from '@core/utils'; import { deepClone, objectHashCode } from '@core/utils';
import { EntityDataSubscription, EntityDataSubscriptionOptions } from '@core/api/entity-data-subscription'; import { EntityDataSubscription, EntityDataSubscriptionOptions } from '@core/api/entity-data-subscription';
import { Observable, of } from 'rxjs';
export interface EntityDataListener { export interface EntityDataListener {
subscriptionType: widgetType; subscriptionType: widgetType;
subscriptionTimewindow: SubscriptionTimewindow; subscriptionTimewindow?: SubscriptionTimewindow;
configDatasource: Datasource; configDatasource: Datasource;
configDatasourceIndex: number; configDatasourceIndex: number;
dataLoaded: (pageData: PageData<EntityData>, data: Array<Array<DataSetHolder>>, datasourceIndex: number) => void; dataLoaded: (pageData: PageData<EntityData>, data: Array<Array<DataSetHolder>>, datasourceIndex: number) => void;
dataUpdated: (data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void; dataUpdated: (data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void;
updateRealtimeSubscription: () => SubscriptionTimewindow; updateRealtimeSubscription?: () => SubscriptionTimewindow;
setRealtimeSubscription: (subscriptionTimewindow: SubscriptionTimewindow) => void; setRealtimeSubscription?: (subscriptionTimewindow: SubscriptionTimewindow) => void;
entityDataSubscriptionKey?: number; subscription?: EntityDataSubscription;
}
export interface EntityDataLoadResult {
pageData: PageData<EntityData>;
data: Array<Array<DataSetHolder>>;
datasourceIndex: number;
} }
@Injectable({ @Injectable({
@ -42,16 +49,48 @@ export interface EntityDataListener {
}) })
export class EntityDataService { export class EntityDataService {
private subscriptions: {[entityDataSubscriptionKey: string]: EntityDataSubscription} = {};
constructor(private telemetryService: TelemetryWebsocketService, constructor(private telemetryService: TelemetryWebsocketService,
private utils: UtilsService) {} private utils: UtilsService) {}
public subscribeToEntityData(listener: EntityDataListener) { public prepareSubscription(listener: EntityDataListener): Observable<EntityDataLoadResult> {
const datasource = listener.configDatasource; const datasource = listener.configDatasource;
if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !datasource.pageLink)) { if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !datasource.pageLink)) {
return of(null);
}
listener.subscription = this.createSubscription(listener,
datasource.pageLink, datasource.keyFilters,
false);
return listener.subscription.subscribe();
}
public startSubscription(listener: EntityDataListener) {
if (listener.subscriptionType === widgetType.timeseries) {
listener.subscription.entityDataSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
}
listener.subscription.start();
}
public subscribeForLatestData(listener: EntityDataListener,
pageLink: EntityDataPageLink,
keyFilters: KeyFilter[]) {
const datasource = listener.configDatasource;
if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !pageLink)) {
return; return;
} }
listener.subscription = this.createSubscription(listener,
pageLink, keyFilters, true);
listener.subscription.subscribe();
}
public stopSubscription(listener: EntityDataListener) {
listener.subscription.unsubscribe();
}
private createSubscription(listener: EntityDataListener,
pageLink: EntityDataPageLink,
keyFilters: KeyFilter[],
isLatestDataSubscription: boolean): EntityDataSubscription {
const datasource = listener.configDatasource;
const subscriptionDataKeys: Array<SubscriptionDataKey> = []; const subscriptionDataKeys: Array<SubscriptionDataKey> = [];
datasource.dataKeys.forEach((dataKey) => { datasource.dataKeys.forEach((dataKey) => {
const subscriptionDataKey: SubscriptionDataKey = { const subscriptionDataKey: SubscriptionDataKey = {
@ -62,47 +101,19 @@ export class EntityDataService {
}; };
subscriptionDataKeys.push(subscriptionDataKey); subscriptionDataKeys.push(subscriptionDataKey);
}); });
const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = { const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = {
datasourceType: datasource.type, datasourceType: datasource.type,
dataKeys: subscriptionDataKeys, dataKeys: subscriptionDataKeys,
type: listener.subscriptionType type: listener.subscriptionType
}; };
if (listener.subscriptionType === widgetType.timeseries) {
entityDataSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
}
if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) {
entityDataSubscriptionOptions.entityFilter = datasource.entityFilter; entityDataSubscriptionOptions.entityFilter = datasource.entityFilter;
entityDataSubscriptionOptions.pageLink = datasource.pageLink; entityDataSubscriptionOptions.pageLink = pageLink;
entityDataSubscriptionOptions.keyFilters = datasource.keyFilters; entityDataSubscriptionOptions.keyFilters = keyFilters;
}
listener.entityDataSubscriptionKey = objectHashCode(entityDataSubscriptionOptions);
let subscription: EntityDataSubscription;
if (this.subscriptions[listener.entityDataSubscriptionKey]) {
subscription = this.subscriptions[listener.entityDataSubscriptionKey];
subscription.syncListener(listener);
} else {
subscription = new EntityDataSubscription(entityDataSubscriptionOptions,
this.telemetryService, this.utils);
this.subscriptions[listener.entityDataSubscriptionKey] = subscription;
subscription.addListener(listener);
subscription.start();
}
}
public unsubscribeFromDatasource(listener: EntityDataListener) {
if (listener.entityDataSubscriptionKey) {
const subscription = this.subscriptions[listener.entityDataSubscriptionKey];
if (subscription) {
subscription.removeListener(listener);
if (!subscription.hasListeners()) {
subscription.unsubscribe();
delete this.subscriptions[listener.entityDataSubscriptionKey];
}
}
listener.entityDataSubscriptionKey = null;
} }
entityDataSubscriptionOptions.isLatestDataSubscription = isLatestDataSubscription;
return new EntityDataSubscription(entityDataSubscriptionOptions,
listener, this.telemetryService, this.utils);
} }
} }

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

@ -98,7 +98,8 @@ export interface IAliasController {
getAliasInfo(aliasId: string): Observable<AliasInfo>; getAliasInfo(aliasId: string): Observable<AliasInfo>;
getEntityAliasId(aliasName: string): string; getEntityAliasId(aliasName: string): string;
getInstantAliasInfo(aliasId: string): AliasInfo; getInstantAliasInfo(aliasId: string): AliasInfo;
resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>>; resolveSingleEntityInfo(aliasId: string): Observable<EntityInfo>;
resolveDatasources(datasources: Array<Datasource>, singleEntity?: boolean): Observable<Array<Datasource>>;
resolveAlarmSource(alarmSource: Datasource): Observable<Datasource>; resolveAlarmSource(alarmSource: Datasource): Observable<Datasource>;
getEntityAliases(): EntityAliases; getEntityAliases(): EntityAliases;
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo); updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo);
@ -202,8 +203,8 @@ export interface WidgetSubscriptionOptions {
alarmsMaxCountLoad?: number; alarmsMaxCountLoad?: number;
alarmsFetchSize?: number; alarmsFetchSize?: number;
datasources?: Array<Datasource>; datasources?: Array<Datasource>;
keyFilters?: Array<KeyFilter>; hasDataPageLink?: boolean;
pageLink?: EntityDataPageLink; singleEntity?: boolean;
targetDeviceAliasIds?: Array<string>; targetDeviceAliasIds?: Array<string>;
targetDeviceIds?: Array<string>; targetDeviceIds?: Array<string>;
useDashboardTimewindow?: boolean; useDashboardTimewindow?: boolean;
@ -264,7 +265,7 @@ export interface IWidgetSubscription {
onAliasesChanged(aliasIds: Array<string>): boolean; onAliasesChanged(aliasIds: Array<string>): boolean;
onDashboardTimewindowChanged(dashboardTimewindow: Timewindow): boolean; onDashboardTimewindowChanged(dashboardTimewindow: Timewindow): void;
updateDataVisibility(index: number): void; updateDataVisibility(index: number): void;
@ -278,6 +279,10 @@ export interface IWidgetSubscription {
subscribe(): void; subscribe(): void;
subscribeForLatestData(datasourceIndex: number,
pageLink: EntityDataPageLink,
keyFilters: KeyFilter[]): void;
isDataResolved(): boolean; isDataResolved(): boolean;
destroy(): void; destroy(): void;

215
ui-ngx/src/app/core/api/widget-subscription.ts

@ -22,7 +22,6 @@ import {
WidgetSubscriptionOptions WidgetSubscriptionOptions
} from '@core/api/widget-api.models'; } from '@core/api/widget-api.models';
import { import {
DataKey,
DataSet, DataSet,
DataSetHolder, DataSetHolder,
Datasource, Datasource,
@ -43,20 +42,18 @@ import {
toHistoryTimewindow, toHistoryTimewindow,
WidgetTimewindow WidgetTimewindow
} from '@app/shared/models/time/time.models'; } from '@app/shared/models/time/time.models';
import { Observable, ReplaySubject, Subject, throwError } from 'rxjs'; import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs';
import { CancelAnimationFrame } from '@core/services/raf.service'; import { CancelAnimationFrame } from '@core/services/raf.service';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models'; import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models';
import { createLabelFromDatasource, deepClone, isDefined, isEqual } from '@core/utils'; import { createLabelFromDatasource, deepClone, isDefined, isEqual } from '@core/utils';
import { AlarmSourceListener } from '@core/http/alarm.service'; import { AlarmSourceListener } from '@core/http/alarm.service';
import { DatasourceListener } from '@core/api/datasource.service';
import { EntityId } from '@app/shared/models/id/entity-id'; import { EntityId } from '@app/shared/models/id/entity-id';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { entityFields } from '@shared/models/entity.models';
import * as moment_ from 'moment'; import * as moment_ from 'moment';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { EntityDataListener } from '@core/api/entity-data.service'; import { EntityDataListener } from '@core/api/entity-data.service';
import { EntityData, EntityDataPageLink, EntityKeyType } from '@shared/models/query/query.models'; import { EntityData, EntityDataPageLink, EntityKeyType, KeyFilter } from '@shared/models/query/query.models';
import { map } from 'rxjs/operators';
const moment = moment_; const moment = moment_;
@ -73,12 +70,14 @@ export class WidgetSubscription implements IWidgetSubscription {
subscriptionTimewindow: SubscriptionTimewindow; subscriptionTimewindow: SubscriptionTimewindow;
useDashboardTimewindow: boolean; useDashboardTimewindow: boolean;
hasDataPageLink: boolean;
singleEntity: boolean;
datasourcePages: PageData<Datasource>[]; datasourcePages: PageData<Datasource>[];
dataPages: PageData<Array<DatasourceData>>[]; dataPages: PageData<Array<DatasourceData>>[];
entityDataListeners: Array<EntityDataListener>; entityDataListeners: Array<EntityDataListener>;
configuredDatasources: Array<Datasource>; configuredDatasources: Array<Datasource>;
initDataSubscriptionSubject: Subject<void>;
data: Array<DatasourceData>; data: Array<DatasourceData>;
datasources: Array<Datasource>; datasources: Array<Datasource>;
// datasourceListeners: Array<DatasourceListener>; // datasourceListeners: Array<DatasourceListener>;
@ -211,6 +210,8 @@ export class WidgetSubscription implements IWidgetSubscription {
// this.datasources = this.ctx.utils.validateDatasources(options.datasources); // this.datasources = this.ctx.utils.validateDatasources(options.datasources);
this.configuredDatasources = this.ctx.utils.validateDatasources(options.datasources); this.configuredDatasources = this.ctx.utils.validateDatasources(options.datasources);
this.entityDataListeners = []; this.entityDataListeners = [];
this.hasDataPageLink = options.hasDataPageLink;
this.singleEntity = options.singleEntity;
// this.datasourceListeners = []; // this.datasourceListeners = [];
this.datasourcePages = []; this.datasourcePages = [];
this.datasources = []; this.datasources = [];
@ -271,11 +272,11 @@ export class WidgetSubscription implements IWidgetSubscription {
const initRpcSubject = new ReplaySubject(); const initRpcSubject = new ReplaySubject();
if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) { if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) {
this.targetDeviceAliasId = this.targetDeviceAliasIds[0]; this.targetDeviceAliasId = this.targetDeviceAliasIds[0];
this.ctx.aliasController.getAliasInfo(this.targetDeviceAliasId).subscribe( this.ctx.aliasController.resolveSingleEntityInfo(this.targetDeviceAliasId).subscribe(
(aliasInfo) => { (entityInfo) => {
if (aliasInfo.currentEntity && aliasInfo.currentEntity.entityType === EntityType.DEVICE) { if (entityInfo && entityInfo.entityType === EntityType.DEVICE) {
this.targetDeviceId = aliasInfo.currentEntity.id; this.targetDeviceId = entityInfo.id;
this.targetDeviceName = aliasInfo.currentEntity.name; this.targetDeviceName = entityInfo.name;
if (this.targetDeviceId) { if (this.targetDeviceId) {
this.rpcEnabled = true; this.rpcEnabled = true;
} else { } else {
@ -348,34 +349,72 @@ export class WidgetSubscription implements IWidgetSubscription {
} }
private initDataSubscription(): Observable<any> { private initDataSubscription(): Observable<any> {
this.initDataSubscriptionSubject = new ReplaySubject(1); const initDataSubscriptionSubject = new ReplaySubject(1);
this.loadStDiff().subscribe(() => { this.loadStDiff().subscribe(() => {
if (!this.ctx.aliasController) { if (!this.ctx.aliasController) {
this.hasResolvedData = true; this.hasResolvedData = true;
// this.configureData(); this.prepareDataSubscriptions().subscribe(
// initDataSubscriptionSubject.next(); () => {
// initDataSubscriptionSubject.complete(); initDataSubscriptionSubject.next();
this.subscribe(); initDataSubscriptionSubject.complete();
}
);
} else { } else {
this.ctx.aliasController.resolveDatasources(this.configuredDatasources).subscribe( this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity).subscribe(
(datasources) => { (datasources) => {
this.configuredDatasources = datasources; this.configuredDatasources = datasources;
/* if (datasources && datasources.length) { this.prepareDataSubscriptions().subscribe(
this.hasResolvedData = true; () => {
}*/ initDataSubscriptionSubject.next();
this.subscribe(); initDataSubscriptionSubject.complete();
// this.configureData(); }
// initDataSubscriptionSubject.next(); );
// initDataSubscriptionSubject.complete();
}, },
(err) => { (err) => {
this.notifyDataLoaded(); this.notifyDataLoaded();
this.initDataSubscriptionSubject.error(err); initDataSubscriptionSubject.error(err);
} }
); );
} }
}); });
return this.initDataSubscriptionSubject.asObservable(); return initDataSubscriptionSubject.asObservable();
}
private prepareDataSubscriptions(): Observable<any> {
if (this.hasDataPageLink) {
this.hasResolvedData = true;
return of(null);
}
const resolveResultObservables = this.configuredDatasources.map((datasource, index) => {
const listener: EntityDataListener = {
subscriptionType: this.type,
configDatasource: datasource,
configDatasourceIndex: index,
dataLoaded: (pageData, data1, datasourceIndex) => {
this.dataLoaded(pageData, data1, datasourceIndex, true)
},
dataUpdated: this.dataUpdated.bind(this),
updateRealtimeSubscription: () => {
this.subscriptionTimewindow = this.updateRealtimeSubscription();
return this.subscriptionTimewindow;
},
setRealtimeSubscription: (subscriptionTimewindow) => {
this.updateRealtimeSubscription(deepClone(subscriptionTimewindow));
}
};
this.entityDataListeners.push(listener);
return this.ctx.entityDataService.prepareSubscription(listener);
});
return forkJoin(resolveResultObservables).pipe(
map((resolveResults) => {
resolveResults.forEach((resolveResult) => {
this.dataLoaded(resolveResult.pageData, resolveResult.data, resolveResult.datasourceIndex, false);
});
this.configureLoadedData();
this.hasResolvedData = true;
this.notifyDataLoaded();
})
);
} }
/* private initDataSubscriptionOld(): Observable<any> { /* private initDataSubscriptionOld(): Observable<any> {
@ -592,13 +631,12 @@ export class WidgetSubscription implements IWidgetSubscription {
}); });
} }
onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow): boolean { onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow) {
if (this.type === widgetType.timeseries || this.type === widgetType.alarm) { if (this.type === widgetType.timeseries || this.type === widgetType.alarm) {
if (this.useDashboardTimewindow) { if (this.useDashboardTimewindow) {
if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
// this.timeWindowConfig = deepClone(newDashboardTimewindow); this.timeWindowConfig = deepClone(newDashboardTimewindow);
// this.update(); this.update();
// TODO:
return true; return true;
} }
} }
@ -785,8 +823,12 @@ export class WidgetSubscription implements IWidgetSubscription {
} }
update() { update() {
this.unsubscribe(); if (this.type === widgetType.rpc || this.type === widgetType.alarm) {
this.subscribe(); this.unsubscribe();
this.subscribe();
} else {
this.dataSubscribe();
}
} }
subscribe(): void { subscribe(): void {
@ -802,6 +844,29 @@ export class WidgetSubscription implements IWidgetSubscription {
} }
} }
subscribeForLatestData(datasourceIndex: number,
pageLink: EntityDataPageLink,
keyFilters: KeyFilter[]): void {
let entityDataListener = this.entityDataListeners[datasourceIndex];
if (entityDataListener) {
this.ctx.entityDataService.stopSubscription(entityDataListener);
}
const datasource = this.configuredDatasources[datasourceIndex];
if (datasource) {
entityDataListener = {
subscriptionType: this.type,
configDatasource: datasource,
configDatasourceIndex: datasourceIndex,
dataLoaded: (pageData, data1, datasourceIndex1) => {
this.dataLoaded(pageData, data1, datasourceIndex1, true)
},
dataUpdated: this.dataUpdated.bind(this)
};
this.entityDataListeners[datasourceIndex] = entityDataListener;
this.ctx.entityDataService.subscribeForLatestData(entityDataListener, pageLink, keyFilters);
}
}
private doSubscribe() { private doSubscribe() {
if (this.type === widgetType.rpc) { if (this.type === widgetType.rpc) {
return; return;
@ -809,6 +874,12 @@ export class WidgetSubscription implements IWidgetSubscription {
if (this.type === widgetType.alarm) { if (this.type === widgetType.alarm) {
this.alarmsSubscribe(); this.alarmsSubscribe();
} else { } else {
this.dataSubscribe();
}
}
private dataSubscribe() {
if (!this.hasDataPageLink) {
this.notifyDataLoading(); this.notifyDataLoading();
if (this.type === widgetType.timeseries && this.timeWindowConfig) { if (this.type === widgetType.timeseries && this.timeWindowConfig) {
this.updateRealtimeSubscription(); this.updateRealtimeSubscription();
@ -819,62 +890,10 @@ export class WidgetSubscription implements IWidgetSubscription {
this.onDataUpdated(); this.onDataUpdated();
} }
} }
// let index = 0;
const forceUpdate = !this.datasources.length; const forceUpdate = !this.datasources.length;
this.configuredDatasources.forEach((datasource, index) => { this.entityDataListeners.forEach((listener) => {
const listener: EntityDataListener = { listener.subscriptionTimewindow = this.subscriptionTimewindow;
subscriptionType: this.type, this.ctx.entityDataService.startSubscription(listener);
subscriptionTimewindow: this.subscriptionTimewindow,
configDatasource: datasource,
configDatasourceIndex: index,
dataLoaded: this.dataLoaded.bind(this),
dataUpdated: this.dataUpdated.bind(this),
updateRealtimeSubscription: () => {
this.subscriptionTimewindow = this.updateRealtimeSubscription();
return this.subscriptionTimewindow;
},
setRealtimeSubscription: (subscriptionTimewindow) => {
this.updateRealtimeSubscription(deepClone(subscriptionTimewindow));
}
};
/*if (this.comparisonEnabled && datasource.isAdditional) {
listener.subscriptionTimewindow = this.timewindowForComparison;
listener.updateRealtimeSubscription = () => {
this.subscriptionTimewindow = this.updateSubscriptionForComparison();
return this.subscriptionTimewindow;
};
listener.setRealtimeSubscription = () => {
this.updateSubscriptionForComparison();
};
}*/
/* let entityFieldKey = false;
for (let a = 0; a < datasource.dataKeys.length; a++) {
if (datasource.dataKeys[a].type !== DataKeyType.entityField) {
this.data[index + a].data = [];
} else {
entityFieldKey = true;
}
}
index += datasource.dataKeys.length;*/
this.entityDataListeners.push(listener);
// this.datasourceListeners.push(listener);
// if (datasource.dataKeys.length) {
// this.ctx.datasourceService.subscribeToDatasource(listener);
// }
this.ctx.entityDataService.subscribeToEntityData(listener);
/* if (datasource.unresolvedStateEntity || entityFieldKey ||
!datasource.dataKeys.length ||
(datasource.type === DatasourceType.entity && !datasource.entityId)
) {
forceUpdate = true;
}*/
}); });
if (forceUpdate) { if (forceUpdate) {
this.notifyDataLoaded(); this.notifyDataLoaded();
@ -1000,7 +1019,9 @@ export class WidgetSubscription implements IWidgetSubscription {
this.alarmsUnsubscribe(); this.alarmsUnsubscribe();
} else { } else {
this.entityDataListeners.forEach((listener) => { this.entityDataListeners.forEach((listener) => {
this.ctx.entityDataService.unsubscribeFromDatasource(listener); if (listener != null) {
this.ctx.entityDataService.stopSubscription(listener);
}
}); });
this.entityDataListeners.length = 0; this.entityDataListeners.length = 0;
this.resetData(); this.resetData();
@ -1129,7 +1150,9 @@ export class WidgetSubscription implements IWidgetSubscription {
return this.timewindowForComparison; return this.timewindowForComparison;
} }
private dataLoaded(pageData: PageData<EntityData>, data: Array<Array<DataSetHolder>>, datasourceIndex: number) { private dataLoaded(pageData: PageData<EntityData>,
data: Array<Array<DataSetHolder>>,
datasourceIndex: number, isUpdate: boolean) {
const datasource = this.configuredDatasources[datasourceIndex]; const datasource = this.configuredDatasources[datasourceIndex];
datasource.dataReceived = true; datasource.dataReceived = true;
const datasources = pageData.data.map((entityData, index) => const datasources = pageData.data.map((entityData, index) =>
@ -1152,14 +1175,8 @@ export class WidgetSubscription implements IWidgetSubscription {
totalPages: pageData.totalPages totalPages: pageData.totalPages
}; };
this.dataPages[datasourceIndex] = datasourceDataPage; this.dataPages[datasourceIndex] = datasourceDataPage;
this.configureLoadedData(); if (isUpdate) {
const readyCount = this.configuredDatasources.filter(d => d.dataReceived).length;
if (this.configuredDatasources.length === readyCount) {
this.hasResolvedData = true;
this.initDataSubscriptionSubject.next();
this.initDataSubscriptionSubject.complete();
this.configureLoadedData(); this.configureLoadedData();
this.notifyDataLoaded();
this.onDataUpdated(true); this.onDataUpdated(true);
} }
} }
@ -1238,6 +1255,9 @@ export class WidgetSubscription implements IWidgetSubscription {
dataKey, dataKey,
data: [] data: []
}; };
if (data && data[keyIndex] && data[keyIndex].data) {
datasourceData.data = data[keyIndex].data;
}
return datasourceData; return datasourceData;
}); });
} }
@ -1275,6 +1295,7 @@ export class WidgetSubscription implements IWidgetSubscription {
const startIndex = configuredDatasource.dataKeyStartIndex; const startIndex = configuredDatasource.dataKeyStartIndex;
const dataKeysCount = configuredDatasource.dataKeys.length; const dataKeysCount = configuredDatasource.dataKeys.length;
const index = startIndex + dataIndex*dataKeysCount + dataKeyIndex; const index = startIndex + dataIndex*dataKeysCount + dataKeyIndex;
this.notifyDataLoaded();
let update = true; let update = true;
let currentData: DataSetHolder; let currentData: DataSetHolder;
if (this.displayLegend && this.legendData.keys[index].dataKey.hidden) { if (this.displayLegend && this.legendData.keys[index].dataKey.hidden) {

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

@ -54,9 +54,15 @@ import {
import { EntityRelationService } from '@core/http/entity-relation.service'; import { EntityRelationService } from '@core/http/entity-relation.service';
import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils'; import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils';
import { Asset, AssetSearchQuery } from '@shared/models/asset.models'; import { Asset, AssetSearchQuery } from '@shared/models/asset.models';
import { Device, DeviceCredentialsType, DeviceSearchQuery } from '@shared/models/device.models'; import { ClaimResult, Device, DeviceCredentialsType, DeviceSearchQuery } from '@shared/models/device.models';
import { EntityViewSearchQuery } from '@shared/models/entity-view.models'; import { EntityViewSearchQuery } from '@shared/models/entity-view.models';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import {
createDefaultEntityDataPageLink,
EntityData,
EntityDataQuery,
EntityFilter, EntityKeyType
} from '@shared/models/query/query.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -360,6 +366,54 @@ export class EntityService {
} }
} }
public findEntityDataByQuery(query: EntityDataQuery, config?: RequestConfig): Observable<PageData<EntityData>> {
return this.http.post<PageData<EntityData>>('/api/entitiesQuery/find', query, defaultHttpOptionsFromConfig(config));
}
private entityDataToEntityInfo(entityData: EntityData): EntityInfo {
const entityInfo: EntityInfo = {
id: entityData.entityId.id,
entityType: entityData.entityId.entityType as EntityType
};
if (entityData.latest && entityData.latest[EntityKeyType.ENTITY_FIELD]) {
const fields = entityData.latest[EntityKeyType.ENTITY_FIELD];
if (fields.name) {
entityInfo.name = fields.name.value;
}
if (fields.label) {
entityInfo.label = fields.label.value;
}
}
return entityInfo;
}
public findSingleEntityInfoByEntityFilter(filter: EntityFilter, config?: RequestConfig): Observable<EntityInfo> {
const query: EntityDataQuery = {
entityFilter: filter,
pageLink: createDefaultEntityDataPageLink(1),
entityFields: [
{
type: EntityKeyType.ENTITY_FIELD,
key: 'name'
},
{
type: EntityKeyType.ENTITY_FIELD,
key: 'label'
}
]
};
return this.findEntityDataByQuery(query, config).pipe(
map((data) => {
if (data.data.length) {
const entityData = data.data[0];
return this.entityDataToEntityInfo(entityData);
} else {
return null;
}
})
);
}
public getAliasFilterTypesByEntityTypes(entityTypes: Array<EntityType | AliasEntityType>): Array<AliasFilterType> { public getAliasFilterTypesByEntityTypes(entityTypes: Array<EntityType | AliasEntityType>): Array<AliasFilterType> {
const allAliasFilterTypes: Array<AliasFilterType> = Object.keys(AliasFilterType).map((key) => AliasFilterType[key]); const allAliasFilterTypes: Array<AliasFilterType> = Object.keys(AliasFilterType).map((key) => AliasFilterType[key]);
if (!entityTypes || !entityTypes.length) { if (!entityTypes || !entityTypes.length) {
@ -605,7 +659,7 @@ export class EntityService {
public resolveAlias(entityAlias: EntityAlias, stateParams: StateParams): Observable<AliasInfo> { public resolveAlias(entityAlias: EntityAlias, stateParams: StateParams): Observable<AliasInfo> {
const filter = entityAlias.filter; const filter = entityAlias.filter;
return this.resolveAliasFilter(filter, stateParams).pipe( return this.resolveAliasFilter(filter, stateParams).pipe(
map((result) => { mergeMap((result) => {
const aliasInfo: AliasInfo = { const aliasInfo: AliasInfo = {
alias: entityAlias.alias, alias: entityAlias.alias,
entityFilter: result.entityFilter, entityFilter: result.entityFilter,
@ -615,30 +669,19 @@ export class EntityService {
}; };
aliasInfo.resolvedEntities = result.entities; aliasInfo.resolvedEntities = result.entities;
aliasInfo.currentEntity = null; aliasInfo.currentEntity = null;
if (aliasInfo.resolvedEntities.length) { if (!aliasInfo.resolveMultiple && aliasInfo.entityFilter) {
aliasInfo.currentEntity = aliasInfo.resolvedEntities[0]; return this.findSingleEntityInfoByEntityFilter(aliasInfo.entityFilter,
{ignoreLoading: true, ignoreErrors: true}).pipe(
map((entity) => {
aliasInfo.currentEntity = entity;
return aliasInfo;
})
);
} }
return aliasInfo; return of(aliasInfo);
}) })
); );
} }
/*
public resolveEntityFilter(filter: EntityAliasFilter, stateParams: StateParams): EntityFilter {
const stateEntityInfo = this.getStateEntityInfo(filter, stateParams);
let result: EntityFilter = filter;
const stateEntityId = stateEntityInfo.entityId;
if (filter.type === AliasFilterType.stateEntity) {
result = {
singleEntity: stateEntityId,
type: AliasFilterType.singleEntity
};
} else if (filter.rootStateEntity) {
let rootEntityType;
let rootEntityId;
}
return result;
}*/
public resolveAliasFilter(filter: EntityAliasFilter, stateParams: StateParams): Observable<EntityAliasFilterResult> { public resolveAliasFilter(filter: EntityAliasFilter, stateParams: StateParams): Observable<EntityAliasFilterResult> {
const result: EntityAliasFilterResult = { const result: EntityAliasFilterResult = {

11
ui-ngx/src/app/core/ws/telemetry-websocket.service.ts

@ -114,6 +114,17 @@ export class TelemetryWebsocketService implements TelemetryService {
this.publishCommands(); this.publishCommands();
} }
public update(subscriber: TelemetrySubscriber) {
subscriber.subscriptionCommands.forEach(
(subscriptionCommand) => {
if (subscriptionCommand.cmdId && subscriptionCommand instanceof EntityDataCmd) {
this.cmdsWrapper.entityDataCmds.push(subscriptionCommand);
}
}
);
this.publishCommands();
}
public unsubscribe(subscriber: TelemetrySubscriber) { public unsubscribe(subscriber: TelemetrySubscriber) {
if (this.isActive) { if (this.isActive) {
subscriber.subscriptionCommands.forEach( subscriber.subscriptionCommands.forEach(

2
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html

@ -39,7 +39,7 @@
</mat-toolbar> </mat-toolbar>
<div fxFlex class="table-container"> <div fxFlex class="table-container">
<table mat-table [dataSource]="entityDatasource" <table mat-table [dataSource]="entityDatasource"
matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear> matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear>
<ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;"> <ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;">
<mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell> <mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell>
<mat-cell *matCellDef="let entity;" <mat-cell *matCellDef="let entity;"

156
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts

@ -32,26 +32,23 @@ import {
DataKey, DataKey,
Datasource, Datasource,
DatasourceData, DatasourceData,
DatasourceType,
WidgetActionDescriptor, WidgetActionDescriptor,
WidgetConfig WidgetConfig
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { IWidgetSubscription } from '@core/api/widget-api.models'; import { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { deepClone, isDefined, isNumber, createLabelFromDatasource, hashCode } from '@core/utils'; import { createLabelFromDatasource, deepClone, hashCode, isDefined, isNumber } from '@core/utils';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs'; import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { entityTypeTranslations } from '@shared/models/entity-type.models'; import { entityTypeTranslations } from '@shared/models/entity-type.models';
import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort, SortDirection } from '@angular/material/sort';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { import {
CellContentInfo, CellContentInfo,
@ -59,15 +56,13 @@ import {
constructTableCssString, constructTableCssString,
DisplayColumn, DisplayColumn,
EntityColumn, EntityColumn,
EntityData, EntityData, entityDataSortOrderFromString, findColumnByEntityKey, findEntityKeyByColumnDef,
fromEntityColumnDef,
getCellContentInfo, getCellContentInfo,
getCellStyleInfo, getCellStyleInfo,
getColumnWidth, getColumnWidth,
getEntityValue, getEntityValue,
TableWidgetDataKeySettings, TableWidgetDataKeySettings,
TableWidgetSettings, TableWidgetSettings,
toEntityColumnDef,
widthStyle widthStyle
} from '@home/components/widget/lib/table-widget.models'; } from '@home/components/widget/lib/table-widget.models';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
@ -77,6 +72,13 @@ import {
DisplayColumnsPanelComponent, DisplayColumnsPanelComponent,
DisplayColumnsPanelData DisplayColumnsPanelData
} from '@home/components/widget/lib/display-columns-panel.component'; } from '@home/components/widget/lib/display-columns-panel.component';
import {
Direction,
EntityDataPageLink,
entityDataPageLinkSortDirection,
EntityKeyType,
KeyFilter
} from '@shared/models/query/query.models';
interface EntitiesTableWidgetSettings extends TableWidgetSettings { interface EntitiesTableWidgetSettings extends TableWidgetSettings {
entitiesTitle: string; entitiesTitle: string;
@ -103,7 +105,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public displayPagination = true; public displayPagination = true;
public pageSizeOptions; public pageSizeOptions;
public pageLink: PageLink; public pageLink: EntityDataPageLink;
public sortOrderProperty: string; public sortOrderProperty: string;
public textSearchMode = false; public textSearchMode = false;
public columns: Array<EntityColumn> = []; public columns: Array<EntityColumn> = [];
@ -150,8 +152,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
private domSanitizer: DomSanitizer) { private domSanitizer: DomSanitizer) {
super(store); super(store);
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); // const sortOrder: EntityDataSortOrder = sortOrderFromString(this.defaultSortOrder);
this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder); this.pageLink = {
page: 0,
pageSize: this.defaultPageSize,
textSearch: null
};
// new PageLink(this.defaultPageSize, 0, null, sortOrder);
} }
ngOnInit(): void { ngOnInit(): void {
@ -191,11 +198,15 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public onDataUpdated() { public onDataUpdated() {
this.ngZone.run(() => { this.ngZone.run(() => {
this.entityDatasource.updateEntitiesData(this.subscription.data); this.entityDatasource.dataUpdated(); // .updateEntitiesData(this.subscription.data);
this.ctx.detectChanges(); this.ctx.detectChanges();
}); });
} }
public pageLinkSortDirection(): SortDirection {
return entityDataPageLinkSortDirection(this.pageLink);
}
private initializeConfig() { private initializeConfig() {
this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction]; this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];
@ -256,7 +267,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
name: 'entityName', name: 'entityName',
label: 'entityName', label: 'entityName',
def: 'entityName', def: 'entityName',
title: entityNameColumnTitle title: entityNameColumnTitle,
entityKey: {
key: 'name',
type: EntityKeyType.ENTITY_FIELD
}
} as EntityColumn } as EntityColumn
); );
this.contentsInfo.entityName = { this.contentsInfo.entityName = {
@ -273,7 +288,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
name: 'entityLabel', name: 'entityLabel',
label: 'entityLabel', label: 'entityLabel',
def: 'entityLabel', def: 'entityLabel',
title: entityLabelColumnTitle title: entityLabelColumnTitle,
entityKey: {
key: 'label',
type: EntityKeyType.ENTITY_FIELD
}
} as EntityColumn } as EntityColumn
); );
this.contentsInfo.entityLabel = { this.contentsInfo.entityLabel = {
@ -291,6 +310,10 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
label: 'entityType', label: 'entityType',
def: 'entityType', def: 'entityType',
title: this.translate.instant('entity.entity-type'), title: this.translate.instant('entity.entity-type'),
entityKey: {
key: 'entityType',
type: EntityKeyType.ENTITY_FIELD
}
} as EntityColumn } as EntityColumn
); );
this.contentsInfo.entityType = { this.contentsInfo.entityType = {
@ -309,8 +332,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
if (datasource) { if (datasource) {
datasource.dataKeys.forEach((entityDataKey) => { datasource.dataKeys.forEach((entityDataKey) => {
const dataKey: EntityColumn = deepClone(entityDataKey) as EntityColumn; const dataKey: EntityColumn = deepClone(entityDataKey) as EntityColumn;
dataKey.entityKey = {
key: dataKey.name,
type: null
};
if (dataKey.type === DataKeyType.function) { if (dataKey.type === DataKeyType.function) {
dataKey.name = dataKey.label; dataKey.name = dataKey.label;
dataKey.entityKey.type = EntityKeyType.ENTITY_FIELD;
} else if (dataKey.type === DataKeyType.entityField) {
dataKey.entityKey.type = EntityKeyType.ENTITY_FIELD;
} else if (dataKey.type === DataKeyType.attribute) {
dataKey.entityKey.type = EntityKeyType.ATTRIBUTE;
} else if (dataKey.type === DataKeyType.timeseries) {
dataKey.entityKey.type = EntityKeyType.TIME_SERIES;
} }
dataKeys.push(dataKey); dataKeys.push(dataKey);
@ -331,14 +365,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {
this.defaultSortOrder = this.settings.defaultSortOrder; this.defaultSortOrder = this.settings.defaultSortOrder;
} }
this.pageLink.sortOrder = sortOrderFromString(this.defaultSortOrder);
this.sortOrderProperty = toEntityColumnDef(this.pageLink.sortOrder.property, this.columns); this.pageLink.sortOrder = entityDataSortOrderFromString(this.defaultSortOrder, this.columns);
let sortColumn: EntityColumn;
if (this.pageLink.sortOrder) {
sortColumn = findColumnByEntityKey(this.pageLink.sortOrder.key, this.columns);
}
this.sortOrderProperty = sortColumn ? sortColumn.def : null;
if (this.actionCellDescriptors.length) { if (this.actionCellDescriptors.length) {
this.displayedColumns.push('actions'); this.displayedColumns.push('actions');
} }
this.entityDatasource = new EntityDatasource( this.entityDatasource = new EntityDatasource(
this.translate, dataKeys, this.subscription.datasources); this.translate, dataKeys, this.subscription);
} }
private editColumnsToDisplay($event: Event) { private editColumnsToDisplay($event: Event) {
@ -416,9 +455,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
} else { } else {
this.pageLink.page = 0; this.pageLink.page = 0;
} }
this.pageLink.sortOrder.property = fromEntityColumnDef(this.sort.active, this.columns); this.pageLink.sortOrder = {
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; key: findEntityKeyByColumnDef(this.sort.active, this.columns),
this.entityDatasource.loadEntities(this.pageLink); direction: Direction[this.sort.direction.toUpperCase()]
};
const keyFilters: KeyFilter[] = null; // TODO:
this.entityDatasource.loadEntities(this.pageLink, keyFilters);
this.ctx.detectChanges(); this.ctx.detectChanges();
} }
@ -523,18 +565,19 @@ class EntityDatasource implements DataSource<EntityData> {
private entitiesSubject = new BehaviorSubject<EntityData[]>([]); private entitiesSubject = new BehaviorSubject<EntityData[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<EntityData>>(emptyPageData<EntityData>()); private pageDataSubject = new BehaviorSubject<PageData<EntityData>>(emptyPageData<EntityData>());
private allEntities: Array<EntityData> = []; // private allEntities: Array<EntityData> = [];
private allEntitiesSubject = new BehaviorSubject<EntityData[]>([]); // private allEntitiesSubject = new BehaviorSubject<EntityData[]>([]);
private allEntities$: Observable<Array<EntityData>> = this.allEntitiesSubject.asObservable(); // private allEntities$: Observable<Array<EntityData>> = this.allEntitiesSubject.asObservable();
private currentEntity: EntityData = null; private currentEntity: EntityData = null;
constructor( constructor(
private translate: TranslateService, private translate: TranslateService,
private dataKeys: Array<DataKey>, private dataKeys: Array<DataKey>,
datasources: Array<Datasource> private subscription: IWidgetSubscription
// datasources: Array<Datasource>
) { ) {
/*
for (const datasource of datasources) { for (const datasource of datasources) {
if (datasource.type === DatasourceType.entity && !datasource.entityId) { if (datasource.type === DatasourceType.entity && !datasource.entityId) {
continue; continue;
@ -558,7 +601,7 @@ class EntityDatasource implements DataSource<EntityData> {
}); });
this.allEntities.push(entity); this.allEntities.push(entity);
} }
this.allEntitiesSubject.next(this.allEntities); this.allEntitiesSubject.next(this.allEntities);*/
} }
connect(collectionViewer: CollectionViewer): Observable<EntityData[] | ReadonlyArray<EntityData>> { connect(collectionViewer: CollectionViewer): Observable<EntityData[] | ReadonlyArray<EntityData>> {
@ -570,18 +613,63 @@ class EntityDatasource implements DataSource<EntityData> {
this.pageDataSubject.complete(); this.pageDataSubject.complete();
} }
loadEntities(pageLink: PageLink) { loadEntities(pageLink: EntityDataPageLink, keyFilters: KeyFilter[]) {
this.fetchEntities(pageLink).pipe( this.subscription.subscribeForLatestData(0, pageLink, keyFilters);
/* this.fetchEntities(pageLink).pipe(
catchError(() => of(emptyPageData<EntityData>())), catchError(() => of(emptyPageData<EntityData>())),
).subscribe( ).subscribe(
(pageData) => { (pageData) => {
this.entitiesSubject.next(pageData.data); this.entitiesSubject.next(pageData.data);
this.pageDataSubject.next(pageData); this.pageDataSubject.next(pageData);
} }
); );*/
} }
updateEntitiesData(data: DatasourceData[]) { dataUpdated() {
const datasourcesPageData = this.subscription.datasourcePages[0];
const dataPageData = this.subscription.dataPages[0];
const entities = new Array<EntityData>();
datasourcesPageData.data.forEach((datasource, index) => {
entities.push(this.datasourceToEntityData(datasource, dataPageData.data[index]));
});
const entitiesPageData: PageData<EntityData> = {
data: entities,
totalPages: datasourcesPageData.totalPages,
totalElements: datasourcesPageData.totalElements,
hasNext: datasourcesPageData.hasNext
};
this.entitiesSubject.next(entities);
this.pageDataSubject.next(entitiesPageData);
}
private datasourceToEntityData(datasource: Datasource, data: DatasourceData[]): EntityData {
const entity: EntityData = {
id: {} as EntityId,
entityName: datasource.entityName,
entityLabel: datasource.entityLabel ? datasource.entityLabel : datasource.entityName
};
if (datasource.entityId) {
entity.id.id = datasource.entityId;
}
if (datasource.entityType) {
entity.id.entityType = datasource.entityType;
entity.entityType = this.translate.instant(entityTypeTranslations.get(datasource.entityType).type);
} else {
entity.entityType = '';
}
this.dataKeys.forEach((dataKey, index) => {
const keyData = data[index].data;
if (keyData && keyData.length && keyData[0].length > 1) {
const value = keyData[0][1];
entity[dataKey.label] = value;
} else {
entity[dataKey.label] = '';
}
});
return entity;
}
/* updateEntitiesData(data: DatasourceData[]) {
for (let i = 0; i < this.allEntities.length; i++) { for (let i = 0; i < this.allEntities.length; i++) {
const entity = this.allEntities[i]; const entity = this.allEntities[i];
for (let a = 0; a < this.dataKeys.length; a++) { for (let a = 0; a < this.dataKeys.length; a++) {
@ -597,7 +685,7 @@ class EntityDatasource implements DataSource<EntityData> {
} }
} }
this.allEntitiesSubject.next(this.allEntities); this.allEntitiesSubject.next(this.allEntities);
} }*/
isEmpty(): Observable<boolean> { isEmpty(): Observable<boolean> {
return this.entitiesSubject.pipe( return this.entitiesSubject.pipe(
@ -625,9 +713,9 @@ class EntityDatasource implements DataSource<EntityData> {
(this.currentEntity.id.id === entity.id.id); (this.currentEntity.id.id === entity.id.id);
} }
private fetchEntities(pageLink: PageLink): Observable<PageData<EntityData>> { /* private fetchEntities(pageLink: PageLink): Observable<PageData<EntityData>> {
return this.allEntities$.pipe( return this.allEntities$.pipe(
map((data) => pageLink.filterData(data)) map((data) => pageLink.filterData(data))
); );
} }*/
} }

58
ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts

@ -19,6 +19,7 @@ import { DataKey, WidgetConfig } from '@shared/models/widget.models';
import { getDescendantProp, isDefined } from '@core/utils'; import { getDescendantProp, isDefined } from '@core/utils';
import { alarmFields, AlarmInfo } from '@shared/models/alarm.models'; import { alarmFields, AlarmInfo } from '@shared/models/alarm.models';
import * as tinycolor_ from 'tinycolor2'; import * as tinycolor_ from 'tinycolor2';
import { Direction, EntityDataSortOrder, EntityKey } from '@shared/models/query/query.models';
const tinycolor = tinycolor_; const tinycolor = tinycolor_;
@ -49,6 +50,7 @@ export interface EntityData {
export interface EntityColumn extends DataKey { export interface EntityColumn extends DataKey {
def: string; def: string;
title: string; title: string;
entityKey?: EntityKey;
} }
export interface DisplayColumn { export interface DisplayColumn {
@ -73,6 +75,58 @@ export interface CellStyleInfo {
cellStyleFunction?: CellStyleFunction; cellStyleFunction?: CellStyleFunction;
} }
export function entityDataSortOrderFromString(strSortOrder: string, columns: EntityColumn[]): EntityDataSortOrder {
if (!strSortOrder && !strSortOrder.length) {
return null;
}
let property: string;
let direction = Direction.ASC;
if (strSortOrder.startsWith('-')) {
direction = Direction.DESC;
property = strSortOrder.substring(1);
} else {
if (strSortOrder.startsWith('+')) {
property = strSortOrder.substring(1);
} else {
property = strSortOrder;
}
}
if (!property && !property.length) {
return null;
}
const column = findColumnByLabel(property, columns);
if (column && column.entityKey) {
return {key: column.entityKey, direction};
}
return null;
}
export function findColumnByEntityKey(key: EntityKey, columns: EntityColumn[]): EntityColumn {
if (key) {
return columns.find(theColumn => theColumn.entityKey &&
theColumn.entityKey.type === key.type && theColumn.entityKey.key === key.key);
} else {
return null;
}
}
export function findEntityKeyByColumnDef(def: string, columns: EntityColumn[]): EntityKey {
return findColumnByDef(def, columns).entityKey;
}
export function findColumn(searchProperty: string, searchValue: string, columns: EntityColumn[]): EntityColumn {
return columns.find(theColumn => theColumn[searchProperty] === searchValue);
}
export function findColumnByLabel(label: string, columns: EntityColumn[]): EntityColumn {
return findColumn('label', label, columns);
}
export function findColumnByDef(def: string, columns: EntityColumn[]): EntityColumn {
return findColumn('def', def, columns);
}
export function findColumnProperty(searchProperty: string, searchValue: string, columnProperty: string, columns: EntityColumn[]): string { export function findColumnProperty(searchProperty: string, searchValue: string, columnProperty: string, columns: EntityColumn[]): string {
let res = searchValue; let res = searchValue;
const column = columns.find(theColumn => theColumn[searchProperty] === searchValue); const column = columns.find(theColumn => theColumn[searchProperty] === searchValue);
@ -82,6 +136,10 @@ export function findColumnProperty(searchProperty: string, searchValue: string,
return res; return res;
} }
export function toEntityKey(def: string, columns: EntityColumn[]): string {
return findColumnProperty('def', def, 'label', columns);
}
export function toEntityColumnDef(label: string, columns: EntityColumn[]): string { export function toEntityColumnDef(label: string, columns: EntityColumn[]): string {
return findColumnProperty('label', label, 'def', columns); return findColumnProperty('label', label, 'def', columns);
} }

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

@ -346,12 +346,19 @@ export class WidgetComponentService {
} else { } else {
result.typeParameters.useCustomDatasources = false; result.typeParameters.useCustomDatasources = false;
} }
if (isUndefined(result.typeParameters.hasDataPageLink)) {
result.typeParameters.hasDataPageLink = false;
}
if (isUndefined(result.typeParameters.maxDatasources)) { if (isUndefined(result.typeParameters.maxDatasources)) {
result.typeParameters.maxDatasources = -1; result.typeParameters.maxDatasources = -1;
} }
if (isUndefined(result.typeParameters.maxDataKeys)) { if (isUndefined(result.typeParameters.maxDataKeys)) {
result.typeParameters.maxDataKeys = -1; result.typeParameters.maxDataKeys = -1;
} }
if (isUndefined(result.typeParameters.singleEntity)) {
result.typeParameters.singleEntity = result.typeParameters.maxDatasources === 1 &&
result.typeParameters.maxDataKeys === 1;
}
if (isUndefined(result.typeParameters.dataKeysOptional)) { if (isUndefined(result.typeParameters.dataKeysOptional)) {
result.typeParameters.dataKeysOptional = false; result.typeParameters.dataKeysOptional = false;
} }

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

@ -620,14 +620,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.rxSubscriptions.push(this.widgetContext.dashboard.dashboardTimewindowChanged.subscribe( this.rxSubscriptions.push(this.widgetContext.dashboard.dashboardTimewindowChanged.subscribe(
(dashboardTimewindow) => { (dashboardTimewindow) => {
// TODO:
let subscriptionChanged = false;
for (const id of Object.keys(this.widgetContext.subscriptions)) { for (const id of Object.keys(this.widgetContext.subscriptions)) {
const subscription = this.widgetContext.subscriptions[id]; const subscription = this.widgetContext.subscriptions[id];
subscriptionChanged = subscriptionChanged || subscription.onDashboardTimewindowChanged(dashboardTimewindow); subscription.onDashboardTimewindowChanged(dashboardTimewindow);
}
if (subscriptionChanged && !this.typeParameters.useCustomDatasources) {
this.reInit();
} }
} }
)); ));
@ -845,6 +840,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
options = { options = {
type: this.widget.type, type: this.widget.type,
stateData: this.typeParameters.stateData, stateData: this.typeParameters.stateData,
hasDataPageLink: this.typeParameters.hasDataPageLink,
singleEntity: this.typeParameters.singleEntity,
comparisonEnabled: comparisonSettings.comparisonEnabled, comparisonEnabled: comparisonSettings.comparisonEnabled,
timeForComparison: comparisonSettings.timeForComparison timeForComparison: comparisonSettings.timeForComparison
}; };

31
ui-ngx/src/app/shared/models/query/query.models.ts

@ -16,6 +16,7 @@
import { AliasFilterType, EntityFilters } from '@shared/models/alias.models'; import { AliasFilterType, EntityFilters } from '@shared/models/alias.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { SortDirection } from '@angular/material/sort';
export enum EntityKeyType { export enum EntityKeyType {
ATTRIBUTE = 'ATTRIBUTE', ATTRIBUTE = 'ATTRIBUTE',
@ -122,18 +123,30 @@ export interface EntityDataPageLink {
sortOrder?: EntityDataSortOrder; sortOrder?: EntityDataSortOrder;
} }
export const defaultEntityDataPageLink: EntityDataPageLink = { export function entityDataPageLinkSortDirection(pageLink: EntityDataPageLink): SortDirection {
pageSize: 1024, if (pageLink.sortOrder) {
page: 0, return (pageLink.sortOrder.direction + '').toLowerCase() as SortDirection;
sortOrder: { } else {
key: { return '' as SortDirection;
type: EntityKeyType.ENTITY_FIELD,
key: 'createdTime'
},
direction: Direction.DESC
} }
} }
export function createDefaultEntityDataPageLink(pageSize: number): EntityDataPageLink {
return {
pageSize,
page: 0,
sortOrder: {
key: {
type: EntityKeyType.ENTITY_FIELD,
key: 'createdTime'
},
direction: Direction.DESC
}
}
}
export const defaultEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1024);
export interface EntityCountQuery { export interface EntityCountQuery {
entityFilter: EntityFilter; entityFilter: EntityFilter;
} }

11
ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts

@ -21,7 +21,7 @@ import { Observable, ReplaySubject, Subject } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { NgZone } from '@angular/core'; import { NgZone } from '@angular/core';
import { EntityData, EntityDataQuery } from '@shared/models/query/query.models'; import { EntityData, EntityDataQuery, EntityKey } from '@shared/models/query/query.models';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
export enum DataKeyType { export enum DataKeyType {
@ -139,7 +139,7 @@ export interface EntityHistoryCmd {
} }
export interface LatestValueCmd { export interface LatestValueCmd {
keys: Array<string>; keys: Array<EntityKey>;
} }
export interface TimeSeriesCmd { export interface TimeSeriesCmd {
@ -153,7 +153,7 @@ export interface TimeSeriesCmd {
export class EntityDataCmd implements WebsocketCmd { export class EntityDataCmd implements WebsocketCmd {
cmdId: number; cmdId: number;
query: EntityDataQuery; query?: EntityDataQuery;
historyCmd?: EntityHistoryCmd; historyCmd?: EntityHistoryCmd;
latestCmd?: LatestValueCmd; latestCmd?: LatestValueCmd;
tsCmd?: TimeSeriesCmd; tsCmd?: TimeSeriesCmd;
@ -314,6 +314,7 @@ export class EntityDataUpdate implements EntityDataUpdateMsg {
export interface TelemetryService { export interface TelemetryService {
subscribe(subscriber: TelemetrySubscriber); subscribe(subscriber: TelemetrySubscriber);
update(subscriber: TelemetrySubscriber);
unsubscribe(subscriber: TelemetrySubscriber); unsubscribe(subscriber: TelemetrySubscriber);
} }
@ -360,6 +361,10 @@ export class TelemetrySubscriber {
this.telemetryService.subscribe(this); this.telemetryService.subscribe(this);
} }
public update() {
this.telemetryService.update(this);
}
public unsubscribe() { public unsubscribe() {
this.telemetryService.unsubscribe(this); this.telemetryService.unsubscribe(this);
this.complete(); this.complete();

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

@ -150,6 +150,8 @@ export interface WidgetTypeParameters {
maxDataKeys?: number; maxDataKeys?: number;
dataKeysOptional?: boolean; dataKeysOptional?: boolean;
stateData?: boolean; stateData?: boolean;
hasDataPageLink?: boolean;
singleEntity?: boolean;
} }
export interface WidgetControllerDescriptor { export interface WidgetControllerDescriptor {

Loading…
Cancel
Save