Browse Source

WS services

pull/3053/head
Andrii Shvaika 6 years ago
parent
commit
51ce039ac4
  1. 32
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 3
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  4. 15
      application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java
  5. 2
      application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java
  6. 6
      application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java
  7. 2
      application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java
  8. 1
      dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java
  9. 54
      ui-ngx/src/app/core/api/alias-controller.ts
  10. 361
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  11. 91
      ui-ngx/src/app/core/api/entity-data.service.ts
  12. 13
      ui-ngx/src/app/core/api/widget-api.models.ts
  13. 215
      ui-ngx/src/app/core/api/widget-subscription.ts
  14. 87
      ui-ngx/src/app/core/http/entity.service.ts
  15. 11
      ui-ngx/src/app/core/ws/telemetry-websocket.service.ts
  16. 2
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html
  17. 156
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  18. 58
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  19. 7
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  20. 9
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  21. 31
      ui-ngx/src/app/shared/models/query/query.models.ts
  22. 11
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  23. 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

3
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java

@ -5,7 +5,7 @@
* 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
* 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,
@ -128,7 +128,6 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
serviceId = serviceInfoProvider.getServiceId();
wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ws-entity-sub-callback"));
tsInSqlDB = databaseTsType.equalsIgnoreCase("sql") || databaseTsType.equalsIgnoreCase("timescale");
}
@PreDestroy

2
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java

@ -5,7 +5,7 @@
* 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
* 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,

15
application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java

@ -1,3 +1,18 @@
/**
* Copyright © 2016-2020 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.
*/
package org.thingsboard.server.service.subscription;
import lombok.Data;

2
application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java

@ -5,7 +5,7 @@
* 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
* 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,

6
application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java

@ -5,7 +5,7 @@
* 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
* 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,
@ -213,13 +213,16 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest {
cmd = new EntityDataCmd(1, edq, null, latestCmd, null);
wrapper = new TelemetryPluginCmdsWrapper();
wrapper.setEntityDataCmds(Collections.singletonList(cmd));
wsClient.send(mapper.writeValueAsString(wrapper));
msg = wsClient.waitForReply();
update = mapper.readValue(msg, EntityDataUpdate.class);
Assert.assertEquals(1, update.getCmdId());
pageData = update.getData();
Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size());
@ -243,6 +246,7 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest {
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsValue);
}
}

2
application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java

@ -5,7 +5,7 @@
* 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
* 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,

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

@ -48,6 +48,7 @@ public class EntityKeyMapping {
static {
entityFieldColumnMap.put("createdTime", "id");
entityFieldColumnMap.put("entityType", "entity_type");
entityFieldColumnMap.put("name", "name");
entityFieldColumnMap.put("type", "type");
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 { AliasFilterType, EntityAliases } from '@shared/models/alias.models';
import { EntityInfo } from '@shared/models/entity.models';
import { map } from 'rxjs/operators';
import { defaultEntityDataPageLink } from '@shared/models/query/query.models';
import { map, mergeMap } from 'rxjs/operators';
import { createDefaultEntityDataPageLink, defaultEntityDataPageLink } from '@shared/models/query/query.models';
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.entityAliasId) {
return this.getAliasInfo(datasource.entityAliasId).pipe(
@ -200,14 +217,14 @@ export class AliasController implements IAliasController {
datasources.push(newDatasource);
}
return datasources;*/
return [newDatasource];
return newDatasource;
} else {
if (aliasInfo.stateEntity) {
newDatasource = deepClone(datasource);
newDatasource.unresolvedStateEntity = true;
return [newDatasource];
return newDatasource;
} else {
return [];
return null;
// throw new Error('Unable to resolve datasource.');
}
}
@ -232,13 +249,13 @@ export class AliasController implements IAliasController {
entityType: entity.entityType
}
};
return [datasource];
return datasource;
} else {
if (aliasInfo.stateEntity) {
datasource.unresolvedStateEntity = true;
return [datasource];
return datasource;
} else {
return [];
return null;
// throw new Error('Unable to resolve datasource.');
}
}
@ -248,10 +265,10 @@ export class AliasController implements IAliasController {
} else {
datasource.aliasName = datasource.entityName;
datasource.name = datasource.entityName;
return of([datasource]);
return of(datasource);
}
} else {
return of([datasource]);
return of(datasource);
}
}
@ -354,18 +371,14 @@ export class AliasController implements IAliasController {
);
}
resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> {
const newDatasources = deepClone(datasources);
const observables = new Array<Observable<Array<Datasource>>>();
resolveDatasources(datasources: Array<Datasource>, singleEntity?: boolean): Observable<Array<Datasource>> {
const newDatasources = deepClone(singleEntity ? [datasources[0]] : datasources);
const observables = new Array<Observable<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);
});
map((result) => {
let functionIndex = 0;
result.forEach((datasource) => {
if (datasource.type === DatasourceType.function) {
@ -386,6 +399,9 @@ export class AliasController implements IAliasController {
datasource.name = 'Unresolved';
datasource.entityName = 'Unresolved';
} else if (datasource.type === DatasourceType.entity) {
if (singleEntity) {
datasource.pageLink = createDefaultEntityDataPageLink(1);
}
if (!datasource.pageLink) {
datasource.pageLink = deepClone(defaultEntityDataPageLink);
}

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

@ -35,19 +35,21 @@ import {
TelemetrySubscriber
} from '@shared/models/telemetry/telemetry.models';
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 { PageData } from '@shared/models/page/page-data';
import { DataAggregator } from '@core/api/data-aggregator';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { EntityType } from '@shared/models/entity-type.models';
import Timeout = NodeJS.Timeout;
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
export interface EntityDataSubscriptionOptions {
datasourceType: DatasourceType;
dataKeys: Array<SubscriptionDataKey>;
type: widgetType;
entityFilter?: EntityFilter;
isLatestDataSubscription?: boolean;
pageLink?: EntityDataPageLink;
keyFilters?: Array<KeyFilter>;
subscriptionTimewindow?: SubscriptionTimewindow;
@ -59,21 +61,19 @@ declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number, dataKeyInd
export class EntityDataSubscription {
private listeners: Array<EntityDataListener> = [];
private datasourceType: DatasourceType = this.entityDataSubscriptionOptions.datasourceType;
private history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
private realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs);
private history: boolean;
private realtime: boolean;
private subscriber: TelemetrySubscriber;
private dataCommand: EntityDataCmd;
private subsCommand: EntityDataCmd;
private attrFields: Array<EntityKey>;
private tsFields: Array<EntityKey>;
private latestValues: Array<EntityKey>;
private entityDataResolveSubject: Subject<EntityDataLoadResult>;
private pageData: PageData<EntityData>;
private subsTw: SubscriptionTimewindow;
private dataAggregators: Array<DataAggregator>;
@ -87,7 +87,11 @@ export class EntityDataSubscription {
private tickElapsed = 0;
private timer: Timeout;
constructor(private entityDataSubscriptionOptions: EntityDataSubscriptionOptions,
private dataResolved = false;
private started = false;
constructor(public entityDataSubscriptionOptions: EntityDataSubscriptionOptions,
private listener: EntityDataListener,
private telemetryService: TelemetryService,
private utils: UtilsService) {
this.initializeSubscription();
@ -126,50 +130,6 @@ export class EntityDataSubscription {
}
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() {
@ -192,19 +152,30 @@ export class EntityDataSubscription {
this.pageData = null;
}
public start() {
this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
public subscribe(): Observable<EntityDataLoadResult> {
if (!this.entityDataSubscriptionOptions.isLatestDataSubscription) {
this.entityDataResolveSubject = new ReplaySubject(1);
} else {
this.started = true;
this.dataResolved = true;
}
if (this.datasourceType === DatasourceType.entity) {
const entityFields: Array<EntityKey> =
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')) {
entityFields.push({
type: EntityKeyType.ENTITY_FIELD,
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(
dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name })
@ -217,9 +188,9 @@ export class EntityDataSubscription {
this.latestValues = this.attrFields.concat(this.tsFields);
this.subscriber = new TelemetrySubscriber(this.telemetryService);
const command = new EntityDataCmd();
this.dataCommand = new EntityDataCmd();
command.query = {
this.dataCommand.query = {
entityFilter: this.entityDataSubscriptionOptions.entityFilter,
pageLink: this.entityDataSubscriptionOptions.pageLink,
keyFilters: this.entityDataSubscriptionOptions.keyFilters,
@ -227,72 +198,17 @@ export class EntityDataSubscription {
latestValues: this.latestValues
};
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (this.tsFields.length > 0) {
if (this.history) {
command.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.entityDataSubscriptionOptions.isLatestDataSubscription) {
if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
if (this.latestValues.length > 0) {
this.dataCommand.latestCmd = {
keys: this.latestValues
};
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(
(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();
} else if (this.datasourceType === DatasourceType.function) {
const entityData: EntityData = {
@ -325,29 +265,46 @@ export class EntityDataSubscription {
totalPages: 1
};
this.onPageData(pageData);
this.tickScheduledTime = this.utils.currentPerfTime();
if (this.history) {
this.onTick(true);
} else {
this.timer = setTimeout(this.onTick.bind(this, true), 0);
if (this.entityDataSubscriptionOptions.isLatestDataSubscription) {
if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
this.frequency = 1000;
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) {
this.dataAggregators.forEach((aggregator) => {
aggregator.destroy();
})
this.dataAggregators = null;
}
this.datasourceData = [];
this.dataAggregators = [];
this.entityIdToDataIndex = {};
let tsKeyNames;
this.resetData();
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
let tsKeyNames = [];
if (this.datasourceType === DatasourceType.function) {
tsKeyNames = [];
for (const key of Object.keys(this.dataKeys)) {
const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
dataKeysList.forEach((subscriptionDataKey) => {
@ -357,20 +314,85 @@ export class EntityDataSubscription {
} else {
tsKeyNames = this.tsFields ? this.tsFields.map(field => field.key) : [];
}
}
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) {
for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
if (this.datasourceType === DatasourceType.function) {
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) {
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)) {
const dataKey = this.dataKeys[key];
if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
@ -388,7 +410,23 @@ export class EntityDataSubscription {
}
}
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>> = [];
for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) {
const entityData = pageData.data[dataIndex];
@ -401,28 +439,33 @@ export class EntityDataSubscription {
}
);
}
this.pageData = pageData;
this.listeners.forEach((listener) => {
listener.dataLoaded(pageData, data,
listener.configDatasourceIndex);
});
if (!this.dataResolved) {
this.dataResolved = true;
this.entityDataResolveSubject.next(
{
pageData,
data,
datasourceIndex: this.listener.configDatasourceIndex
}
);
this.entityDataResolveSubject.complete();
} else {
this.listener.dataLoaded(pageData, data,
this.listener.configDatasourceIndex);
}
}
private onDataUpdate(update: Array<EntityData>) {
for (const entityData of update) {
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) {
this.listeners.forEach((listener) => {
listener.dataUpdated(data,
listener.configDatasourceIndex,
private notifyListener(data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) {
this.listener.dataUpdated(data,
this.listener.configDatasourceIndex,
dataIndex, dataKeyIndex, detectChanges);
});
}
private processEntityData(entityData: EntityData, dataIndex: number, aggregate: boolean,
@ -596,14 +639,10 @@ export class EntityDataSubscription {
const value = dataKey.func(time, prevSeries[1]);
const series: [number, any] = [time, value];
this.datasourceData[0][dataKey.key].data = [series];
this.listeners.forEach(
(listener) => {
listener.dataUpdated(this.datasourceData[0][dataKey.key],
listener.configDatasourceIndex,
0,
dataKey.index, detectChanges);
}
);
this.listener.dataUpdated(this.datasourceData[0][dataKey.key],
this.listener.configDatasourceIndex,
0,
dataKey.index, detectChanges);
}
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 { deepClone, objectHashCode } from '@core/utils';
import { EntityDataSubscription, EntityDataSubscriptionOptions } from '@core/api/entity-data-subscription';
import { Observable, of } from 'rxjs';
export interface EntityDataListener {
subscriptionType: widgetType;
subscriptionTimewindow: SubscriptionTimewindow;
subscriptionTimewindow?: SubscriptionTimewindow;
configDatasource: Datasource;
configDatasourceIndex: number;
dataLoaded: (pageData: PageData<EntityData>, data: Array<Array<DataSetHolder>>, datasourceIndex: number) => void;
dataUpdated: (data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void;
updateRealtimeSubscription: () => SubscriptionTimewindow;
setRealtimeSubscription: (subscriptionTimewindow: SubscriptionTimewindow) => void;
entityDataSubscriptionKey?: number;
updateRealtimeSubscription?: () => SubscriptionTimewindow;
setRealtimeSubscription?: (subscriptionTimewindow: SubscriptionTimewindow) => void;
subscription?: EntityDataSubscription;
}
export interface EntityDataLoadResult {
pageData: PageData<EntityData>;
data: Array<Array<DataSetHolder>>;
datasourceIndex: number;
}
@Injectable({
@ -42,16 +49,48 @@ export interface EntityDataListener {
})
export class EntityDataService {
private subscriptions: {[entityDataSubscriptionKey: string]: EntityDataSubscription} = {};
constructor(private telemetryService: TelemetryWebsocketService,
private utils: UtilsService) {}
public subscribeToEntityData(listener: EntityDataListener) {
public prepareSubscription(listener: EntityDataListener): Observable<EntityDataLoadResult> {
const datasource = listener.configDatasource;
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;
}
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> = [];
datasource.dataKeys.forEach((dataKey) => {
const subscriptionDataKey: SubscriptionDataKey = {
@ -62,47 +101,19 @@ export class EntityDataService {
};
subscriptionDataKeys.push(subscriptionDataKey);
});
const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = {
datasourceType: datasource.type,
dataKeys: subscriptionDataKeys,
type: listener.subscriptionType
};
if (listener.subscriptionType === widgetType.timeseries) {
entityDataSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
}
if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) {
entityDataSubscriptionOptions.entityFilter = datasource.entityFilter;
entityDataSubscriptionOptions.pageLink = datasource.pageLink;
entityDataSubscriptionOptions.keyFilters = datasource.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.pageLink = pageLink;
entityDataSubscriptionOptions.keyFilters = keyFilters;
}
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>;
getEntityAliasId(aliasName: string): string;
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>;
getEntityAliases(): EntityAliases;
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo);
@ -202,8 +203,8 @@ export interface WidgetSubscriptionOptions {
alarmsMaxCountLoad?: number;
alarmsFetchSize?: number;
datasources?: Array<Datasource>;
keyFilters?: Array<KeyFilter>;
pageLink?: EntityDataPageLink;
hasDataPageLink?: boolean;
singleEntity?: boolean;
targetDeviceAliasIds?: Array<string>;
targetDeviceIds?: Array<string>;
useDashboardTimewindow?: boolean;
@ -264,7 +265,7 @@ export interface IWidgetSubscription {
onAliasesChanged(aliasIds: Array<string>): boolean;
onDashboardTimewindowChanged(dashboardTimewindow: Timewindow): boolean;
onDashboardTimewindowChanged(dashboardTimewindow: Timewindow): void;
updateDataVisibility(index: number): void;
@ -278,6 +279,10 @@ export interface IWidgetSubscription {
subscribe(): void;
subscribeForLatestData(datasourceIndex: number,
pageLink: EntityDataPageLink,
keyFilters: KeyFilter[]): void;
isDataResolved(): boolean;
destroy(): void;

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

@ -22,7 +22,6 @@ import {
WidgetSubscriptionOptions
} from '@core/api/widget-api.models';
import {
DataKey,
DataSet,
DataSetHolder,
Datasource,
@ -43,20 +42,18 @@ import {
toHistoryTimewindow,
WidgetTimewindow
} 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 { EntityType } from '@shared/models/entity-type.models';
import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models';
import { createLabelFromDatasource, deepClone, isDefined, isEqual } from '@core/utils';
import { AlarmSourceListener } from '@core/http/alarm.service';
import { DatasourceListener } from '@core/api/datasource.service';
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 { PageData } from '@shared/models/page/page-data';
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_;
@ -73,12 +70,14 @@ export class WidgetSubscription implements IWidgetSubscription {
subscriptionTimewindow: SubscriptionTimewindow;
useDashboardTimewindow: boolean;
hasDataPageLink: boolean;
singleEntity: boolean;
datasourcePages: PageData<Datasource>[];
dataPages: PageData<Array<DatasourceData>>[];
entityDataListeners: Array<EntityDataListener>;
configuredDatasources: Array<Datasource>;
initDataSubscriptionSubject: Subject<void>;
data: Array<DatasourceData>;
datasources: Array<Datasource>;
// datasourceListeners: Array<DatasourceListener>;
@ -211,6 +210,8 @@ export class WidgetSubscription implements IWidgetSubscription {
// this.datasources = this.ctx.utils.validateDatasources(options.datasources);
this.configuredDatasources = this.ctx.utils.validateDatasources(options.datasources);
this.entityDataListeners = [];
this.hasDataPageLink = options.hasDataPageLink;
this.singleEntity = options.singleEntity;
// this.datasourceListeners = [];
this.datasourcePages = [];
this.datasources = [];
@ -271,11 +272,11 @@ export class WidgetSubscription implements IWidgetSubscription {
const initRpcSubject = new ReplaySubject();
if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) {
this.targetDeviceAliasId = this.targetDeviceAliasIds[0];
this.ctx.aliasController.getAliasInfo(this.targetDeviceAliasId).subscribe(
(aliasInfo) => {
if (aliasInfo.currentEntity && aliasInfo.currentEntity.entityType === EntityType.DEVICE) {
this.targetDeviceId = aliasInfo.currentEntity.id;
this.targetDeviceName = aliasInfo.currentEntity.name;
this.ctx.aliasController.resolveSingleEntityInfo(this.targetDeviceAliasId).subscribe(
(entityInfo) => {
if (entityInfo && entityInfo.entityType === EntityType.DEVICE) {
this.targetDeviceId = entityInfo.id;
this.targetDeviceName = entityInfo.name;
if (this.targetDeviceId) {
this.rpcEnabled = true;
} else {
@ -348,34 +349,72 @@ export class WidgetSubscription implements IWidgetSubscription {
}
private initDataSubscription(): Observable<any> {
this.initDataSubscriptionSubject = new ReplaySubject(1);
const initDataSubscriptionSubject = new ReplaySubject(1);
this.loadStDiff().subscribe(() => {
if (!this.ctx.aliasController) {
this.hasResolvedData = true;
// this.configureData();
// initDataSubscriptionSubject.next();
// initDataSubscriptionSubject.complete();
this.subscribe();
this.prepareDataSubscriptions().subscribe(
() => {
initDataSubscriptionSubject.next();
initDataSubscriptionSubject.complete();
}
);
} else {
this.ctx.aliasController.resolveDatasources(this.configuredDatasources).subscribe(
this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity).subscribe(
(datasources) => {
this.configuredDatasources = datasources;
/* if (datasources && datasources.length) {
this.hasResolvedData = true;
}*/
this.subscribe();
// this.configureData();
// initDataSubscriptionSubject.next();
// initDataSubscriptionSubject.complete();
this.prepareDataSubscriptions().subscribe(
() => {
initDataSubscriptionSubject.next();
initDataSubscriptionSubject.complete();
}
);
},
(err) => {
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> {
@ -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.useDashboardTimewindow) {
if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
// this.timeWindowConfig = deepClone(newDashboardTimewindow);
// this.update();
// TODO:
this.timeWindowConfig = deepClone(newDashboardTimewindow);
this.update();
return true;
}
}
@ -785,8 +823,12 @@ export class WidgetSubscription implements IWidgetSubscription {
}
update() {
this.unsubscribe();
this.subscribe();
if (this.type === widgetType.rpc || this.type === widgetType.alarm) {
this.unsubscribe();
this.subscribe();
} else {
this.dataSubscribe();
}
}
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() {
if (this.type === widgetType.rpc) {
return;
@ -809,6 +874,12 @@ export class WidgetSubscription implements IWidgetSubscription {
if (this.type === widgetType.alarm) {
this.alarmsSubscribe();
} else {
this.dataSubscribe();
}
}
private dataSubscribe() {
if (!this.hasDataPageLink) {
this.notifyDataLoading();
if (this.type === widgetType.timeseries && this.timeWindowConfig) {
this.updateRealtimeSubscription();
@ -819,62 +890,10 @@ export class WidgetSubscription implements IWidgetSubscription {
this.onDataUpdated();
}
}
// let index = 0;
const forceUpdate = !this.datasources.length;
this.configuredDatasources.forEach((datasource, index) => {
const listener: EntityDataListener = {
subscriptionType: this.type,
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;
}*/
this.entityDataListeners.forEach((listener) => {
listener.subscriptionTimewindow = this.subscriptionTimewindow;
this.ctx.entityDataService.startSubscription(listener);
});
if (forceUpdate) {
this.notifyDataLoaded();
@ -1000,7 +1019,9 @@ export class WidgetSubscription implements IWidgetSubscription {
this.alarmsUnsubscribe();
} else {
this.entityDataListeners.forEach((listener) => {
this.ctx.entityDataService.unsubscribeFromDatasource(listener);
if (listener != null) {
this.ctx.entityDataService.stopSubscription(listener);
}
});
this.entityDataListeners.length = 0;
this.resetData();
@ -1129,7 +1150,9 @@ export class WidgetSubscription implements IWidgetSubscription {
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];
datasource.dataReceived = true;
const datasources = pageData.data.map((entityData, index) =>
@ -1152,14 +1175,8 @@ export class WidgetSubscription implements IWidgetSubscription {
totalPages: pageData.totalPages
};
this.dataPages[datasourceIndex] = datasourceDataPage;
this.configureLoadedData();
const readyCount = this.configuredDatasources.filter(d => d.dataReceived).length;
if (this.configuredDatasources.length === readyCount) {
this.hasResolvedData = true;
this.initDataSubscriptionSubject.next();
this.initDataSubscriptionSubject.complete();
if (isUpdate) {
this.configureLoadedData();
this.notifyDataLoaded();
this.onDataUpdated(true);
}
}
@ -1238,6 +1255,9 @@ export class WidgetSubscription implements IWidgetSubscription {
dataKey,
data: []
};
if (data && data[keyIndex] && data[keyIndex].data) {
datasourceData.data = data[keyIndex].data;
}
return datasourceData;
});
}
@ -1275,6 +1295,7 @@ export class WidgetSubscription implements IWidgetSubscription {
const startIndex = configuredDatasource.dataKeyStartIndex;
const dataKeysCount = configuredDatasource.dataKeys.length;
const index = startIndex + dataIndex*dataKeysCount + dataKeyIndex;
this.notifyDataLoaded();
let update = true;
let currentData: DataSetHolder;
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 { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils';
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 { AttributeService } from '@core/http/attribute.service';
import {
createDefaultEntityDataPageLink,
EntityData,
EntityDataQuery,
EntityFilter, EntityKeyType
} from '@shared/models/query/query.models';
@Injectable({
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> {
const allAliasFilterTypes: Array<AliasFilterType> = Object.keys(AliasFilterType).map((key) => AliasFilterType[key]);
if (!entityTypes || !entityTypes.length) {
@ -605,7 +659,7 @@ export class EntityService {
public resolveAlias(entityAlias: EntityAlias, stateParams: StateParams): Observable<AliasInfo> {
const filter = entityAlias.filter;
return this.resolveAliasFilter(filter, stateParams).pipe(
map((result) => {
mergeMap((result) => {
const aliasInfo: AliasInfo = {
alias: entityAlias.alias,
entityFilter: result.entityFilter,
@ -615,30 +669,19 @@ export class EntityService {
};
aliasInfo.resolvedEntities = result.entities;
aliasInfo.currentEntity = null;
if (aliasInfo.resolvedEntities.length) {
aliasInfo.currentEntity = aliasInfo.resolvedEntities[0];
if (!aliasInfo.resolveMultiple && aliasInfo.entityFilter) {
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> {
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();
}
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) {
if (this.isActive) {
subscriber.subscriptionCommands.forEach(

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

@ -39,7 +39,7 @@
</mat-toolbar>
<div fxFlex class="table-container">
<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;">
<mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell>
<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,
Datasource,
DatasourceData,
DatasourceType,
WidgetActionDescriptor,
WidgetConfig
} from '@shared/models/widget.models';
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service';
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 { 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 { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id';
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 { MatSort } from '@angular/material/sort';
import { MatSort, SortDirection } from '@angular/material/sort';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import {
CellContentInfo,
@ -59,15 +56,13 @@ import {
constructTableCssString,
DisplayColumn,
EntityColumn,
EntityData,
fromEntityColumnDef,
EntityData, entityDataSortOrderFromString, findColumnByEntityKey, findEntityKeyByColumnDef,
getCellContentInfo,
getCellStyleInfo,
getColumnWidth,
getEntityValue,
TableWidgetDataKeySettings,
TableWidgetSettings,
toEntityColumnDef,
widthStyle
} from '@home/components/widget/lib/table-widget.models';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
@ -77,6 +72,13 @@ import {
DisplayColumnsPanelComponent,
DisplayColumnsPanelData
} 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 {
entitiesTitle: string;
@ -103,7 +105,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public displayPagination = true;
public pageSizeOptions;
public pageLink: PageLink;
public pageLink: EntityDataPageLink;
public sortOrderProperty: string;
public textSearchMode = false;
public columns: Array<EntityColumn> = [];
@ -150,8 +152,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
private domSanitizer: DomSanitizer) {
super(store);
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);
this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder);
// const sortOrder: EntityDataSortOrder = sortOrderFromString(this.defaultSortOrder);
this.pageLink = {
page: 0,
pageSize: this.defaultPageSize,
textSearch: null
};
// new PageLink(this.defaultPageSize, 0, null, sortOrder);
}
ngOnInit(): void {
@ -191,11 +198,15 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public onDataUpdated() {
this.ngZone.run(() => {
this.entityDatasource.updateEntitiesData(this.subscription.data);
this.entityDatasource.dataUpdated(); // .updateEntitiesData(this.subscription.data);
this.ctx.detectChanges();
});
}
public pageLinkSortDirection(): SortDirection {
return entityDataPageLinkSortDirection(this.pageLink);
}
private initializeConfig() {
this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];
@ -256,7 +267,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
name: 'entityName',
label: 'entityName',
def: 'entityName',
title: entityNameColumnTitle
title: entityNameColumnTitle,
entityKey: {
key: 'name',
type: EntityKeyType.ENTITY_FIELD
}
} as EntityColumn
);
this.contentsInfo.entityName = {
@ -273,7 +288,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
name: 'entityLabel',
label: 'entityLabel',
def: 'entityLabel',
title: entityLabelColumnTitle
title: entityLabelColumnTitle,
entityKey: {
key: 'label',
type: EntityKeyType.ENTITY_FIELD
}
} as EntityColumn
);
this.contentsInfo.entityLabel = {
@ -291,6 +310,10 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
label: 'entityType',
def: 'entityType',
title: this.translate.instant('entity.entity-type'),
entityKey: {
key: 'entityType',
type: EntityKeyType.ENTITY_FIELD
}
} as EntityColumn
);
this.contentsInfo.entityType = {
@ -309,8 +332,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
if (datasource) {
datasource.dataKeys.forEach((entityDataKey) => {
const dataKey: EntityColumn = deepClone(entityDataKey) as EntityColumn;
dataKey.entityKey = {
key: dataKey.name,
type: null
};
if (dataKey.type === DataKeyType.function) {
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);
@ -331,14 +365,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {
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) {
this.displayedColumns.push('actions');
}
this.entityDatasource = new EntityDatasource(
this.translate, dataKeys, this.subscription.datasources);
this.translate, dataKeys, this.subscription);
}
private editColumnsToDisplay($event: Event) {
@ -416,9 +455,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
} else {
this.pageLink.page = 0;
}
this.pageLink.sortOrder.property = fromEntityColumnDef(this.sort.active, this.columns);
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.entityDatasource.loadEntities(this.pageLink);
this.pageLink.sortOrder = {
key: findEntityKeyByColumnDef(this.sort.active, this.columns),
direction: Direction[this.sort.direction.toUpperCase()]
};
const keyFilters: KeyFilter[] = null; // TODO:
this.entityDatasource.loadEntities(this.pageLink, keyFilters);
this.ctx.detectChanges();
}
@ -523,18 +565,19 @@ class EntityDatasource implements DataSource<EntityData> {
private entitiesSubject = new BehaviorSubject<EntityData[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<EntityData>>(emptyPageData<EntityData>());
private allEntities: Array<EntityData> = [];
private allEntitiesSubject = new BehaviorSubject<EntityData[]>([]);
private allEntities$: Observable<Array<EntityData>> = this.allEntitiesSubject.asObservable();
// private allEntities: Array<EntityData> = [];
// private allEntitiesSubject = new BehaviorSubject<EntityData[]>([]);
// private allEntities$: Observable<Array<EntityData>> = this.allEntitiesSubject.asObservable();
private currentEntity: EntityData = null;
constructor(
private translate: TranslateService,
private dataKeys: Array<DataKey>,
datasources: Array<Datasource>
private subscription: IWidgetSubscription
// datasources: Array<Datasource>
) {
/*
for (const datasource of datasources) {
if (datasource.type === DatasourceType.entity && !datasource.entityId) {
continue;
@ -558,7 +601,7 @@ class EntityDatasource implements DataSource<EntityData> {
});
this.allEntities.push(entity);
}
this.allEntitiesSubject.next(this.allEntities);
this.allEntitiesSubject.next(this.allEntities);*/
}
connect(collectionViewer: CollectionViewer): Observable<EntityData[] | ReadonlyArray<EntityData>> {
@ -570,18 +613,63 @@ class EntityDatasource implements DataSource<EntityData> {
this.pageDataSubject.complete();
}
loadEntities(pageLink: PageLink) {
this.fetchEntities(pageLink).pipe(
loadEntities(pageLink: EntityDataPageLink, keyFilters: KeyFilter[]) {
this.subscription.subscribeForLatestData(0, pageLink, keyFilters);
/* this.fetchEntities(pageLink).pipe(
catchError(() => of(emptyPageData<EntityData>())),
).subscribe(
(pageData) => {
this.entitiesSubject.next(pageData.data);
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++) {
const entity = this.allEntities[i];
for (let a = 0; a < this.dataKeys.length; a++) {
@ -597,7 +685,7 @@ class EntityDatasource implements DataSource<EntityData> {
}
}
this.allEntitiesSubject.next(this.allEntities);
}
}*/
isEmpty(): Observable<boolean> {
return this.entitiesSubject.pipe(
@ -625,9 +713,9 @@ class EntityDatasource implements DataSource<EntityData> {
(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(
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 { alarmFields, AlarmInfo } from '@shared/models/alarm.models';
import * as tinycolor_ from 'tinycolor2';
import { Direction, EntityDataSortOrder, EntityKey } from '@shared/models/query/query.models';
const tinycolor = tinycolor_;
@ -49,6 +50,7 @@ export interface EntityData {
export interface EntityColumn extends DataKey {
def: string;
title: string;
entityKey?: EntityKey;
}
export interface DisplayColumn {
@ -73,6 +75,58 @@ export interface CellStyleInfo {
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 {
let res = searchValue;
const column = columns.find(theColumn => theColumn[searchProperty] === searchValue);
@ -82,6 +136,10 @@ export function findColumnProperty(searchProperty: string, searchValue: string,
return res;
}
export function toEntityKey(def: string, columns: EntityColumn[]): string {
return findColumnProperty('def', def, 'label', columns);
}
export function toEntityColumnDef(label: string, columns: EntityColumn[]): string {
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 {
result.typeParameters.useCustomDatasources = false;
}
if (isUndefined(result.typeParameters.hasDataPageLink)) {
result.typeParameters.hasDataPageLink = false;
}
if (isUndefined(result.typeParameters.maxDatasources)) {
result.typeParameters.maxDatasources = -1;
}
if (isUndefined(result.typeParameters.maxDataKeys)) {
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)) {
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(
(dashboardTimewindow) => {
// TODO:
let subscriptionChanged = false;
for (const id of Object.keys(this.widgetContext.subscriptions)) {
const subscription = this.widgetContext.subscriptions[id];
subscriptionChanged = subscriptionChanged || subscription.onDashboardTimewindowChanged(dashboardTimewindow);
}
if (subscriptionChanged && !this.typeParameters.useCustomDatasources) {
this.reInit();
subscription.onDashboardTimewindowChanged(dashboardTimewindow);
}
}
));
@ -845,6 +840,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
options = {
type: this.widget.type,
stateData: this.typeParameters.stateData,
hasDataPageLink: this.typeParameters.hasDataPageLink,
singleEntity: this.typeParameters.singleEntity,
comparisonEnabled: comparisonSettings.comparisonEnabled,
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 { EntityId } from '@shared/models/id/entity-id';
import { SortDirection } from '@angular/material/sort';
export enum EntityKeyType {
ATTRIBUTE = 'ATTRIBUTE',
@ -122,18 +123,30 @@ export interface EntityDataPageLink {
sortOrder?: EntityDataSortOrder;
}
export const defaultEntityDataPageLink: EntityDataPageLink = {
pageSize: 1024,
page: 0,
sortOrder: {
key: {
type: EntityKeyType.ENTITY_FIELD,
key: 'createdTime'
},
direction: Direction.DESC
export function entityDataPageLinkSortDirection(pageLink: EntityDataPageLink): SortDirection {
if (pageLink.sortOrder) {
return (pageLink.sortOrder.direction + '').toLowerCase() as SortDirection;
} else {
return '' as SortDirection;
}
}
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 {
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 { map } from 'rxjs/operators';
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';
export enum DataKeyType {
@ -139,7 +139,7 @@ export interface EntityHistoryCmd {
}
export interface LatestValueCmd {
keys: Array<string>;
keys: Array<EntityKey>;
}
export interface TimeSeriesCmd {
@ -153,7 +153,7 @@ export interface TimeSeriesCmd {
export class EntityDataCmd implements WebsocketCmd {
cmdId: number;
query: EntityDataQuery;
query?: EntityDataQuery;
historyCmd?: EntityHistoryCmd;
latestCmd?: LatestValueCmd;
tsCmd?: TimeSeriesCmd;
@ -314,6 +314,7 @@ export class EntityDataUpdate implements EntityDataUpdateMsg {
export interface TelemetryService {
subscribe(subscriber: TelemetrySubscriber);
update(subscriber: TelemetrySubscriber);
unsubscribe(subscriber: TelemetrySubscriber);
}
@ -360,6 +361,10 @@ export class TelemetrySubscriber {
this.telemetryService.subscribe(this);
}
public update() {
this.telemetryService.update(this);
}
public unsubscribe() {
this.telemetryService.unsubscribe(this);
this.complete();

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

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

Loading…
Cancel
Save