Browse Source

Merge branch 'feature/entity-count-api' of github.com:thingsboard/thingsboard into feature/entity-count-api

pull/4190/head
Andrii Shvaika 6 years ago
parent
commit
2b7111516c
  1. 1
      ui-ngx/package.json
  2. 21
      ui-ngx/src/app/core/api/alias-controller.ts
  3. 79
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  4. 7
      ui-ngx/src/app/core/api/entity-data.service.ts
  5. 8
      ui-ngx/src/app/core/http/entity.service.ts
  6. 4
      ui-ngx/src/app/core/services/item-buffer.service.ts
  7. 3
      ui-ngx/src/app/core/services/utils.service.ts
  8. 32
      ui-ngx/src/app/core/ws/telemetry-websocket.service.ts
  9. 1
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  10. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  11. 4
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html
  12. 9
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts
  13. 10
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.html
  14. 59
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts
  15. 14
      ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts
  16. 11
      ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts
  17. 170
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  18. 40
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
  19. 51
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  20. 1
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts
  21. 2
      ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts
  22. 9
      ui-ngx/src/app/shared/models/query/query.models.ts
  23. 111
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  24. 6
      ui-ngx/src/app/shared/models/widget.models.ts
  25. 6
      ui-ngx/src/app/shared/shared.module.ts
  26. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  27. 7
      ui-ngx/yarn.lock

1
ui-ngx/package.json

@ -68,6 +68,7 @@
"ngx-clipboard": "^13.0.1", "ngx-clipboard": "^13.0.1",
"ngx-color-picker": "^10.1.0", "ngx-color-picker": "^10.1.0",
"ngx-daterangepicker-material": "^4.0.1", "ngx-daterangepicker-material": "^4.0.1",
"ngx-drag-drop": "^2.0.0",
"ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master", "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master",
"ngx-hm-carousel": "^2.0.0-rc.1", "ngx-hm-carousel": "^2.0.0-rc.1",
"ngx-sharebuttons": "^8.0.1", "ngx-sharebuttons": "^8.0.1",

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

@ -16,7 +16,7 @@
import { AliasInfo, IAliasController, StateControllerHolder, StateEntityInfo } from '@core/api/widget-api.models'; import { AliasInfo, IAliasController, StateControllerHolder, StateEntityInfo } from '@core/api/widget-api.models';
import { forkJoin, Observable, of, ReplaySubject, Subject } from 'rxjs'; import { forkJoin, Observable, of, ReplaySubject, Subject } from 'rxjs';
import { Datasource, DatasourceType } from '@app/shared/models/widget.models'; import { Datasource, DatasourceType, datasourceTypeTranslationMap } from '@app/shared/models/widget.models';
import { deepClone, isEqual } from '@core/utils'; import { deepClone, isEqual } from '@core/utils';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
@ -27,6 +27,7 @@ import {
defaultEntityDataPageLink, Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, defaultEntityDataPageLink, Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink,
updateDatasourceFromEntityInfo updateDatasourceFromEntityInfo
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { TranslateService } from '@ngx-translate/core';
export class AliasController implements IAliasController { export class AliasController implements IAliasController {
@ -50,6 +51,7 @@ export class AliasController implements IAliasController {
constructor(private utils: UtilsService, constructor(private utils: UtilsService,
private entityService: EntityService, private entityService: EntityService,
private translate: TranslateService,
private stateControllerHolder: StateControllerHolder, private stateControllerHolder: StateControllerHolder,
private origEntityAliases: EntityAliases, private origEntityAliases: EntityAliases,
private origFilters: Filters) { private origFilters: Filters) {
@ -241,7 +243,7 @@ export class AliasController implements IAliasController {
private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> { private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> {
const newDatasource = deepClone(datasource); const newDatasource = deepClone(datasource);
if (newDatasource.type === DatasourceType.entity) { if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount) {
if (newDatasource.filterId) { if (newDatasource.filterId) {
newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId); newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId);
} }
@ -329,16 +331,23 @@ export class AliasController implements IAliasController {
return forkJoin(observables).pipe( return forkJoin(observables).pipe(
map((result) => { map((result) => {
let functionIndex = 0; let functionIndex = 0;
let entityCountIndex = 0;
result.forEach((datasource) => { result.forEach((datasource) => {
if (datasource.type === DatasourceType.function) { if (datasource.type === DatasourceType.function || datasource.type === DatasourceType.entityCount) {
let name: string; let name: string;
if (datasource.name && datasource.name.length) { if (datasource.name && datasource.name.length) {
name = datasource.name; name = datasource.name;
} else { } else {
functionIndex++; if (datasource.type === DatasourceType.function) {
name = DatasourceType.function; functionIndex++;
if (functionIndex > 1) { } else {
entityCountIndex++;
}
name = this.translate.instant(datasourceTypeTranslationMap.get(datasource.type));
if (datasource.type === DatasourceType.function && functionIndex > 1) {
name += ' ' + functionIndex; name += ' ' + functionIndex;
} else if (datasource.type === DatasourceType.entityCount && entityCountIndex > 1) {
name += ' ' + entityCountIndex;
} }
} }
datasource.name = name; datasource.name = name;

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

@ -22,12 +22,14 @@ import {
EntityFilter, EntityFilter,
EntityKey, EntityKey,
EntityKeyType, EntityKeyType,
entityKeyTypeToDataKeyType, entityPageDataChanged, entityKeyTypeToDataKeyType,
entityPageDataChanged,
KeyFilter, KeyFilter,
TsValue TsValue
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { import {
DataKeyType, DataKeyType,
EntityCountCmd,
EntityDataCmd, EntityDataCmd,
SubscriptionData, SubscriptionData,
SubscriptionDataHolder, SubscriptionDataHolder,
@ -42,6 +44,7 @@ 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 { Observable, of, ReplaySubject, Subject } from 'rxjs'; import { Observable, of, ReplaySubject, Subject } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
declare type DataKeyFunction = (time: number, prevValue: any) => any; declare type DataKeyFunction = (time: number, prevValue: any) => any;
@ -82,6 +85,7 @@ export class EntityDataSubscription {
private subscriber: TelemetrySubscriber; private subscriber: TelemetrySubscriber;
private dataCommand: EntityDataCmd; private dataCommand: EntityDataCmd;
private subsCommand: EntityDataCmd; private subsCommand: EntityDataCmd;
private countCommand: EntityCountCmd;
private attrFields: Array<EntityKey>; private attrFields: Array<EntityKey>;
private tsFields: Array<EntityKey>; private tsFields: Array<EntityKey>;
@ -91,7 +95,7 @@ export class EntityDataSubscription {
private pageData: PageData<EntityData>; private pageData: PageData<EntityData>;
private subsTw: SubscriptionTimewindow; private subsTw: SubscriptionTimewindow;
private dataAggregators: Array<DataAggregator>; private dataAggregators: Array<DataAggregator>;
private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {} private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {};
private datasourceData: {[index: number]: {[key: string]: DataSetHolder}}; private datasourceData: {[index: number]: {[key: string]: DataSetHolder}};
private datasourceOrigData: {[index: number]: {[key: string]: DataSetHolder}}; private datasourceOrigData: {[index: number]: {[key: string]: DataSetHolder}};
private entityIdToDataIndex: {[id: string]: number}; private entityIdToDataIndex: {[id: string]: number};
@ -125,7 +129,8 @@ export class EntityDataSubscription {
} }
} }
let key: string; let key: string;
if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount ||
this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (this.datasourceType === DatasourceType.function) { if (this.datasourceType === DatasourceType.function) {
key = `${dataKey.name}_${dataKey.index}_${dataKey.type}`; key = `${dataKey.name}_${dataKey.index}_${dataKey.type}`;
} else { } else {
@ -159,7 +164,7 @@ export class EntityDataSubscription {
if (this.dataAggregators) { if (this.dataAggregators) {
this.dataAggregators.forEach((aggregator) => { this.dataAggregators.forEach((aggregator) => {
aggregator.destroy(); aggregator.destroy();
}) });
this.dataAggregators = null; this.dataAggregators = null;
} }
this.pageData = null; this.pageData = null;
@ -291,6 +296,65 @@ export class EntityDataSubscription {
totalPages: 1 totalPages: 1
}; };
this.onPageData(pageData); this.onPageData(pageData);
} else if (this.datasourceType === DatasourceType.entityCount) {
this.subscriber = new TelemetrySubscriber(this.telemetryService);
this.countCommand = new EntityCountCmd();
let keyFilters = this.entityDataSubscriptionOptions.keyFilters;
if (this.entityDataSubscriptionOptions.additionalKeyFilters) {
if (keyFilters) {
keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters);
} else {
keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters;
}
}
this.countCommand.query = {
entityFilter: this.entityDataSubscriptionOptions.entityFilter,
keyFilters
};
this.subscriber.subscriptionCommands.push(this.countCommand);
const entityId: EntityId = {
id: NULL_UUID,
entityType: null
};
const entityData: EntityData = {
entityId,
timeseries: {},
latest: {}
};
entityData.latest[EntityKeyType.ENTITY_FIELD] = {
name: {ts: Date.now(), value: DatasourceType.entityCount},
};
const countKey = this.entityDataSubscriptionOptions.dataKeys[0];
this.subscriber.entityCount$.subscribe(
(entityCountUpdate) => {
if (!entityData.latest[EntityKeyType.COUNT]) {
entityData.latest[EntityKeyType.COUNT] = {};
entityData.latest[EntityKeyType.COUNT][countKey.name] = {
ts: Date.now(),
value: entityCountUpdate.count + ''
};
const pageData: PageData<EntityData> = {
data: [entityData],
hasNext: false,
totalElements: 1,
totalPages: 1
};
this.onPageData(pageData);
} else {
const update = [deepClone(entityData)];
update[0].latest[EntityKeyType.COUNT][countKey.name] = {
ts: Date.now(),
value: entityCountUpdate.count + ''
};
this.onDataUpdate(update);
}
}
);
this.subscriber.subscribe();
} }
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
return of(null); return of(null);
@ -347,7 +411,7 @@ export class EntityDataSubscription {
limit: this.subsTw.aggregation.limit, limit: this.subsTw.aggregation.limit,
agg: this.subsTw.aggregation.type, agg: this.subsTw.aggregation.type,
fetchLatestPreviousPoint: this.subsTw.aggregation.stateData fetchLatestPreviousPoint: this.subsTw.aggregation.stateData
} };
} }
} }
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
@ -381,7 +445,7 @@ export class EntityDataSubscription {
if (this.dataAggregators) { if (this.dataAggregators) {
this.dataAggregators.forEach((aggregator) => { this.dataAggregators.forEach((aggregator) => {
aggregator.destroy(); aggregator.destroy();
}) });
} }
this.dataAggregators = []; this.dataAggregators = [];
this.resetData(); this.resetData();
@ -419,7 +483,8 @@ export class EntityDataSubscription {
this.datasourceData[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.datasourceType === DatasourceType.entityCount ||
this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
const dataKeysList = dataKey as Array<SubscriptionDataKey>; const dataKeysList = dataKey as Array<SubscriptionDataKey>;
for (let index = 0; index < dataKeysList.length; index++) { for (let index = 0; index < dataKeysList.length; index++) {
this.datasourceData[dataIndex][key + '_' + index] = { this.datasourceData[dataIndex][key + '_' + index] = {

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

@ -135,11 +135,14 @@ export class EntityDataService {
dataKeys: subscriptionDataKeys, dataKeys: subscriptionDataKeys,
type: subscriptionType type: subscriptionType
}; };
if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity ||
entityDataSubscriptionOptions.datasourceType === DatasourceType.entityCount) {
entityDataSubscriptionOptions.entityFilter = datasource.entityFilter; entityDataSubscriptionOptions.entityFilter = datasource.entityFilter;
entityDataSubscriptionOptions.pageLink = pageLink;
entityDataSubscriptionOptions.keyFilters = keyFilters; entityDataSubscriptionOptions.keyFilters = keyFilters;
entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters;
if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) {
entityDataSubscriptionOptions.pageLink = pageLink;
}
} }
entityDataSubscriptionOptions.isPaginatedDataSubscription = isPaginatedDataSubscription; entityDataSubscriptionOptions.isPaginatedDataSubscription = isPaginatedDataSubscription;
return entityDataSubscriptionOptions; return entityDataSubscriptionOptions;

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

@ -1099,10 +1099,10 @@ export class EntityService {
dataKeys: [] dataKeys: []
}; };
this.prepareEntityFilterFromSubscriptionInfo(datasource, subscriptionInfo); this.prepareEntityFilterFromSubscriptionInfo(datasource, subscriptionInfo);
} else if (subscriptionInfo.type === DatasourceType.function) { } else if (subscriptionInfo.type === DatasourceType.function || subscriptionInfo.type === DatasourceType.entityCount) {
datasource = { datasource = {
type: subscriptionInfo.type, type: subscriptionInfo.type,
name: subscriptionInfo.name || DatasourceType.function, name: subscriptionInfo.name || subscriptionInfo.type,
dataKeys: [] dataKeys: []
}; };
} }
@ -1119,6 +1119,10 @@ export class EntityService {
if (subscriptionInfo.alarmFields) { if (subscriptionInfo.alarmFields) {
this.createDatasourceKeys(subscriptionInfo.alarmFields, DataKeyType.alarm, datasource); this.createDatasourceKeys(subscriptionInfo.alarmFields, DataKeyType.alarm, datasource);
} }
if (subscriptionInfo.type === DatasourceType.entityCount) {
const dataKey = this.utils.createKey({ name: 'count'}, DataKeyType.count);
datasource.dataKeys.push(dataKey);
}
} }
return datasource; return datasource;
} }

4
ui-ngx/src/app/core/services/item-buffer.service.ts

@ -93,7 +93,7 @@ export class ItemBufferService {
if (widget.config.datasources) { if (widget.config.datasources) {
for (let i = 0; i < widget.config.datasources.length; i++) { for (let i = 0; i < widget.config.datasources.length; i++) {
const datasource = widget.config.datasources[i]; const datasource = widget.config.datasources[i];
if (datasource.type === DatasourceType.entity && datasource.entityAliasId) { if ((datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) && datasource.entityAliasId) {
entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId]; entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId];
if (entityAlias) { if (entityAlias) {
aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias); aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias);
@ -119,7 +119,7 @@ export class ItemBufferService {
if (widget.config.datasources) { if (widget.config.datasources) {
for (let i = 0; i < widget.config.datasources.length; i++) { for (let i = 0; i < widget.config.datasources.length; i++) {
const datasource = widget.config.datasources[i]; const datasource = widget.config.datasources[i];
if (datasource.type === DatasourceType.entity && datasource.filterId) { if ((datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) && datasource.filterId) {
filter = dashboard.configuration.filters[datasource.filterId]; filter = dashboard.configuration.filters[datasource.filterId];
if (filter) { if (filter) {
filtersInfo.datasourceFilters[i] = this.prepareFilterInfo(filter); filtersInfo.datasourceFilters[i] = this.prepareFilterInfo(filter);

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

@ -27,7 +27,8 @@ import {
deleteNullProperties, deleteNullProperties,
guid, guid,
isDefined, isDefined,
isDefinedAndNotNull, isString, isDefinedAndNotNull,
isString,
isUndefined isUndefined
} from '@core/utils'; } from '@core/utils';
import { WindowMessage } from '@shared/models/window-message.model'; import { WindowMessage } from '@shared/models/window-message.model';

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

@ -16,18 +16,27 @@
import { Inject, Injectable, NgZone } from '@angular/core'; import { Inject, Injectable, NgZone } from '@angular/core';
import { import {
AlarmDataCmd, AlarmDataUnsubscribeCmd, AlarmDataCmd,
AlarmDataUnsubscribeCmd,
AlarmDataUpdate, AlarmDataUpdate,
AttributesSubscriptionCmd, EntityDataCmd, EntityDataUnsubscribeCmd, EntityDataUpdate, AttributesSubscriptionCmd,
GetHistoryCmd, isAlarmDataUpdateMsg, isEntityDataUpdateMsg, EntityCountCmd, EntityCountUnsubscribeCmd,
EntityCountUpdate,
EntityDataCmd,
EntityDataUnsubscribeCmd,
EntityDataUpdate,
GetHistoryCmd,
isAlarmDataUpdateMsg,
isEntityCountUpdateMsg,
isEntityDataUpdateMsg,
SubscriptionCmd, SubscriptionCmd,
SubscriptionUpdate, SubscriptionUpdate,
SubscriptionUpdateMsg,
TelemetryFeature, TelemetryFeature,
TelemetryPluginCmdsWrapper, TelemetryPluginCmdsWrapper,
TelemetryService, TelemetryService,
TelemetrySubscriber, TelemetrySubscriber,
TimeseriesSubscriptionCmd, WebsocketDataMsg TimeseriesSubscriptionCmd,
WebsocketDataMsg
} from '@app/shared/models/telemetry/telemetry.models'; } from '@app/shared/models/telemetry/telemetry.models';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -111,6 +120,8 @@ export class TelemetryWebsocketService implements TelemetryService {
this.cmdsWrapper.entityDataCmds.push(subscriptionCommand); this.cmdsWrapper.entityDataCmds.push(subscriptionCommand);
} else if (subscriptionCommand instanceof AlarmDataCmd) { } else if (subscriptionCommand instanceof AlarmDataCmd) {
this.cmdsWrapper.alarmDataCmds.push(subscriptionCommand); this.cmdsWrapper.alarmDataCmds.push(subscriptionCommand);
} else if (subscriptionCommand instanceof EntityCountCmd) {
this.cmdsWrapper.entityCountCmds.push(subscriptionCommand);
} }
} }
); );
@ -150,6 +161,10 @@ export class TelemetryWebsocketService implements TelemetryService {
const alarmDataUnsubscribeCmd = new AlarmDataUnsubscribeCmd(); const alarmDataUnsubscribeCmd = new AlarmDataUnsubscribeCmd();
alarmDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; alarmDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdsWrapper.alarmDataUnsubscribeCmds.push(alarmDataUnsubscribeCmd); this.cmdsWrapper.alarmDataUnsubscribeCmds.push(alarmDataUnsubscribeCmd);
} else if (subscriptionCommand instanceof EntityCountCmd) {
const entityCountUnsubscribeCmd = new EntityCountUnsubscribeCmd();
entityCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdsWrapper.entityCountUnsubscribeCmds.push(entityCountUnsubscribeCmd);
} }
const cmdId = subscriptionCommand.cmdId; const cmdId = subscriptionCommand.cmdId;
if (cmdId) { if (cmdId) {
@ -236,7 +251,7 @@ export class TelemetryWebsocketService implements TelemetryService {
{ {
url: uri, url: uri,
openObserver: { openObserver: {
next: (e: Event) => { next: () => {
this.onOpen(); this.onOpen();
} }
}, },
@ -294,6 +309,11 @@ export class TelemetryWebsocketService implements TelemetryService {
if (subscriber) { if (subscriber) {
subscriber.onAlarmData(new AlarmDataUpdate(message)); subscriber.onAlarmData(new AlarmDataUpdate(message));
} }
} else if (isEntityCountUpdateMsg(message)) {
subscriber = this.subscribersMap.get(message.cmdId);
if (subscriber) {
subscriber.onEntityCount(new EntityCountUpdate(message));
}
} else if (message.subscriptionId) { } else if (message.subscriptionId) {
subscriber = this.subscribersMap.get(message.subscriptionId); subscriber = this.subscribersMap.get(message.subscriptionId);
if (subscriber) { if (subscriber) {

1
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts

@ -403,6 +403,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
this.aliasController = new AliasController(this.utils, this.aliasController = new AliasController(this.utils,
this.entityService, this.entityService,
this.translate,
() => stateController, entitiAliases, filters); () => stateController, entitiAliases, filters);
const dataKeyType: DataKeyType = this.attributeScope === LatestTelemetry.LATEST_TELEMETRY ? const dataKeyType: DataKeyType = this.attributeScope === LatestTelemetry.LATEST_TELEMETRY ?

1
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -312,6 +312,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.dashboardCtx.aliasController = new AliasController(this.utils, this.dashboardCtx.aliasController = new AliasController(this.utils,
this.entityService, this.entityService,
this.translate,
() => this.dashboardCtx.stateController, () => this.dashboardCtx.stateController,
this.dashboardConfiguration.entityAliases, this.dashboardConfiguration.entityAliases,
this.dashboardConfiguration.filters); this.dashboardConfiguration.filters);

4
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html

@ -25,7 +25,7 @@
formControlName="name" formControlName="name"
required required
[matAutocomplete]="keyAutocomplete"> [matAutocomplete]="keyAutocomplete">
<button *ngIf="dataKeyFormGroup.get('name').value" <button *ngIf="dataKeyFormGroup.get('name').value && !dataKeyFormGroup.get('name').disabled"
type="button" type="button"
matSuffix mat-icon-button aria-label="Clear" matSuffix mat-icon-button aria-label="Clear"
(click)="clearKey()"> (click)="clearKey()">
@ -72,7 +72,7 @@
formControlName="funcBody"> formControlName="funcBody">
</tb-js-func> </tb-js-func>
</section> </section>
<section fxLayout="column" *ngIf="(modelValue.type === dataKeyTypes.timeseries || modelValue.type === dataKeyTypes.attribute) && showPostProcessing"> <section fxLayout="column" *ngIf="(modelValue.type === dataKeyTypes.timeseries || modelValue.type === dataKeyTypes.attribute || modelValue.type === dataKeyTypes.count) && showPostProcessing">
<mat-checkbox formControlName="usePostProcessing"> <mat-checkbox formControlName="usePostProcessing">
{{ 'datakey.use-data-post-processing-func' | translate }} {{ 'datakey.use-data-post-processing-func' | translate }}
</mat-checkbox> </mat-checkbox>

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

@ -176,7 +176,14 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
this.modelValue.usePostProcessing = true; this.modelValue.usePostProcessing = true;
} }
this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false}); this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false});
this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function ? [Validators.required] : []); this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function &&
this.modelValue.type !== DataKeyType.count
? [Validators.required] : []);
if (this.modelValue.type === DataKeyType.count) {
this.dataKeyFormGroup.get('name').disable({emitEvent: false});
} else {
this.dataKeyFormGroup.get('name').enable({emitEvent: false});
}
this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false}); this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false});
if (this.displayAdvanced) { if (this.displayAdvanced) {
this.dataKeySettingsData.model = this.modelValue.settings; this.dataKeySettingsData.model = this.modelValue.settings;

10
ui-ngx/src/app/modules/home/components/widget/data-keys.component.html

@ -20,7 +20,7 @@
<mat-chip <mat-chip
*ngFor="let key of keys; let $index = index" *ngFor="let key of keys; let $index = index"
[selectable]="!disabled" [selectable]="!disabled"
[removable]="!disabled" [removable]="!disabled && datasourceType !== datasourceTypes.entityCount"
(removed)="remove(key)"> (removed)="remove(key)">
<div fxLayout="row" fxLayoutAlign="start center" class="tb-attribute-chip"> <div fxLayout="row" fxLayoutAlign="start center" class="tb-attribute-chip">
<div *ngIf="!disabled" class="tb-chip-drag-handle" style="margin-right: 5px;"> <div *ngIf="!disabled" class="tb-chip-drag-handle" style="margin-right: 5px;">
@ -31,7 +31,7 @@
</div> </div>
<div class="tb-chip-labels"> <div class="tb-chip-labels">
<div class="tb-chip-label"> <div class="tb-chip-label">
<span *ngIf="datasourceType !== datasourceTypes.function"> <span *ngIf="datasourceType !== datasourceTypes.function && datasourceType !== datasourceTypes.entityCount">
<span *ngIf="key.type === dataKeyTypes.alarm" <span *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}" matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
@ -70,7 +70,7 @@
(click)="editDataKey(key, $index)" mat-button mat-icon-button class="tb-mat-32"> (click)="editDataKey(key, $index)" mat-button mat-icon-button class="tb-mat-32">
<mat-icon class="tb-mat-20">edit</mat-icon> <mat-icon class="tb-mat-20">edit</mat-icon>
</button> </button>
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon> <mat-icon matChipRemove *ngIf="!disabled && datasourceType !== datasourceTypes.entityCount">close</mat-icon>
</div> </div>
</mat-chip> </mat-chip>
<input matInput type="text" placeholder="{{ !disabled ? placeholder : '' }}" <input matInput type="text" placeholder="{{ !disabled ? placeholder : '' }}"
@ -91,7 +91,7 @@
[displayWith]="displayKeyFn"> [displayWith]="displayKeyFn">
<mat-option *ngFor="let key of filteredKeys | async" [value]="key"> <mat-option *ngFor="let key of filteredKeys | async" [value]="key">
<span style="white-space: nowrap;"> <span style="white-space: nowrap;">
<span *ngIf="datasourceType !== datasourceTypes.function"> <span *ngIf="datasourceType !== datasourceTypes.function && datasourceType !== datasourceTypes.entityCount">
<span *ngIf="key.type === dataKeyTypes.alarm" <span *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}" matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
@ -128,7 +128,7 @@
{{ translate.get('entity.no-key-matching', {{ translate.get('entity.no-key-matching',
{key: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }} {key: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span> </span>
<span *ngIf="datasourceType === datasourceTypes.function; else createEntityKey"> <span *ngIf="datasourceType === datasourceTypes.function || datasourceType === datasourceTypes.entityCount; else createEntityKey">
<a translate (click)="createKey(searchText)">entity.create-new-key</a> <a translate (click)="createKey(searchText)">entity.create-new-key</a>
</span> </span>
<ng-template #createEntityKey> <ng-template #createEntityKey>

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

@ -16,26 +16,26 @@
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { import {
AfterViewInit, AfterViewInit,
Component, Component,
ElementRef, ElementRef,
forwardRef, forwardRef,
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
SimpleChanges, SimpleChanges,
SkipSelf, SkipSelf,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
FormControl, FormControl,
FormGroup, FormGroup,
FormGroupDirective, FormGroupDirective,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
NgForm, NgForm,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators';
@ -56,8 +56,8 @@ import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { import {
DataKeyConfigDialogComponent, DataKeyConfigDialogComponent,
DataKeyConfigDialogData DataKeyConfigDialogData
} from '@home/components/widget/data-key-config-dialog.component'; } from '@home/components/widget/data-key-config-dialog.component';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive'; import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive';
@ -94,8 +94,15 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
@Input() @Input()
datasourceType: DatasourceType; datasourceType: DatasourceType;
private maxDataKeysValue: number;
get maxDataKeys(): number {
return this.datasourceType === DatasourceType.entityCount ? 1 : this.maxDataKeysValue;
}
@Input() @Input()
maxDataKeys: number; set maxDataKeys(value: number) {
this.maxDataKeysValue = value;
}
@Input() @Input()
optDataKeys: boolean; optDataKeys: boolean;
@ -114,7 +121,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {
return this.requiredValue || !this.optDataKeys; return this.requiredValue || !this.optDataKeys || this.datasourceType === DatasourceType.entityCount;
} }
@Input() @Input()
set required(value: boolean) { set required(value: boolean) {
@ -210,8 +217,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
if (this.maxDataKeys !== null && this.maxDataKeys > -1) { if (this.maxDataKeys !== null && this.maxDataKeys > -1) {
if (this.datasourceType === DatasourceType.function) { if (this.datasourceType === DatasourceType.function) {
return this.translate.instant('datakey.maximum-function-types', {count: this.maxDataKeys}); return this.translate.instant('datakey.maximum-function-types', {count: this.maxDataKeys});
} else { } else if (this.datasourceType !== DatasourceType.entityCount) {
return this.translate.instant('datakey.maximum-timeseries-or-attributes', {count: this.maxDataKeys}); return this.translate.instant('datakey.maximum-timeseries-or-attributes', {count: this.maxDataKeys});
} else {
return '';
} }
} else { } else {
return ''; return '';
@ -241,6 +250,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
private reset() { private reset() {
if (this.widgetType === widgetType.alarm) { if (this.widgetType === widgetType.alarm) {
this.keys = this.utils.getDefaultAlarmDataKeys(); this.keys = this.utils.getDefaultAlarmDataKeys();
} else if (this.datasourceType === DatasourceType.entityCount) {
this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count)];
} else { } else {
this.keys = []; this.keys = [];
} }
@ -426,7 +437,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys;
fetchObservable = of(targetKeysList); fetchObservable = of(targetKeysList);
} else { } else {
if (this.entityAliasId) { if (this.datasourceType !== DatasourceType.entityCount && this.entityAliasId) {
const dataKeyTypes = [DataKeyType.timeseries]; const dataKeyTypes = [DataKeyType.timeseries];
if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) {
dataKeyTypes.push(DataKeyType.attribute); dataKeyTypes.push(DataKeyType.attribute);

14
ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts

@ -84,7 +84,6 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
private widgetConfig: WidgetConfig; private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription; private subscription: IWidgetSubscription;
private datasources: Array<HierarchyNodeDatasource>; private datasources: Array<HierarchyNodeDatasource>;
private data: Array<Array<DatasourceData>>;
private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {}; private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {};
private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {}; private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {};
@ -121,7 +120,6 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
this.widgetConfig = this.ctx.widgetConfig; this.widgetConfig = this.ctx.widgetConfig;
this.subscription = this.ctx.defaultSubscription; this.subscription = this.ctx.defaultSubscription;
this.datasources = this.subscription.datasources as Array<HierarchyNodeDatasource>; this.datasources = this.subscription.datasources as Array<HierarchyNodeDatasource>;
this.data = this.subscription.dataPages[0].data;
this.initializeConfig(); this.initializeConfig();
this.ctx.updateWidgetParams(); this.ctx.updateWidgetParams();
} }
@ -252,12 +250,16 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
public loadNodes: LoadNodesCallback = (node, cb) => { public loadNodes: LoadNodesCallback = (node, cb) => {
if (node.id === '#') { if (node.id === '#') {
const childNodes: HierarchyNavTreeNode[] = []; const childNodes: HierarchyNavTreeNode[] = [];
let dataIndex = 0;
this.datasources.forEach((childDatasource, index) => { this.datasources.forEach((childDatasource, index) => {
childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, this.data[index])); const datasourceData = this.subscription.data.slice(dataIndex);
childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, datasourceData));
dataIndex += childDatasource.dataKeys.length;
}); });
cb(this.prepareNodes(childNodes)); cb(this.prepareNodes(childNodes));
} else { } else {
if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.nodeCtx.entity.id.entityType !== 'function') { if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.datasource.type === DatasourceType.entity
&& node.data.nodeCtx.entity.id.entityType !== 'function') {
this.loadChildren(node, node.data.datasource, cb); this.loadChildren(node, node.data.datasource, cb);
/* (error) => { // TODO: /* (error) => { // TODO:
let errorText = 'Failed to get relations!'; let errorText = 'Failed to get relations!';
@ -369,8 +371,8 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
} }
private datasourceToNode(datasource: HierarchyNodeDatasource, private datasourceToNode(datasource: HierarchyNodeDatasource,
data: DatasourceData[], data: DatasourceData[],
parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode { parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode {
const node: HierarchyNavTreeNode = { const node: HierarchyNavTreeNode = {
id: (++this.nodeIdCounter) + '' id: (++this.nodeIdCounter) + ''
}; };

11
ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts

@ -153,7 +153,16 @@ export const defaultNodeOpenedFunction: NodeOpenedFunction = nodeCtx => {
}; };
export const defaultNodesSortFunction: NodesSortFunction = (nodeCtx1, nodeCtx2) => { export const defaultNodesSortFunction: NodesSortFunction = (nodeCtx1, nodeCtx2) => {
let result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); let result = 0;
if (!nodeCtx1.entity.id.entityType || !nodeCtx2.entity.id.entityType ) {
if (nodeCtx1.entity.id.entityType) {
result = 1;
} else if (nodeCtx2.entity.id.entityType) {
result = -1;
}
} else {
result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);
}
if (result === 0) { if (result === 0) {
result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name); result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);
} }

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

@ -100,87 +100,115 @@
</div> </div>
<ng-template #datasourcesTemplate> <ng-template #datasourcesTemplate>
<div fxFlex fxLayout="row" fxLayoutAlign="start center"> <div fxFlex fxLayout="row" fxLayoutAlign="start center">
<span fxFlex="5"></span> <span style="width: 60px;"></span>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" <div fxFlex fxLayout="row" fxLayoutAlign="start center"
style="padding: 0 0 0 10px; margin: 5px;"> style="padding: 0 0 0 10px; margin: 5px;">
<span translate style="min-width: 110px;">widget-config.datasource-type</span> <span translate style="min-width: 120px;">widget-config.datasource-type</span>
<span fxHide fxShow.gt-sm translate fxFlex <span fxHide fxShow.gt-sm translate fxFlex
style="padding-left: 10px;">widget-config.datasource-parameters</span> style="padding-left: 10px;">widget-config.datasource-parameters</span>
<span style="min-width: 40px;"></span> <span style="min-width: 40px;"></span>
</div> </div>
</div> </div>
<div style="overflow: auto; padding-bottom: 15px;"> <div style="overflow: auto; padding-bottom: 15px;">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" <mat-list dndDropzone dndEffectAllowed="move"
formArrayName="datasources" (dndDrop)="onDatasourceDrop($event)"
*ngFor="let datasourceControl of datasourcesFormArray().controls; let $index = index;"> [dndDisableIf]="disabled" formArrayName="datasources">
<span fxFlex="5">{{$index + 1}}.</span> <mat-list-item dndPlaceholderRef
<div [formGroupName]="$index" class="mat-elevation-z4" fxFlex class="dndPlaceholder">
fxLayout="row" </mat-list-item>
fxLayoutAlign="start center" <mat-list-item *ngFor="let datasourceControl of datasourcesFormArray().controls; let $index = index;"
style="padding: 0 0 0 10px; margin: 5px;"> [dndDraggable]="datasourceControl.value"
<section fxFlex (dndMoved)="dndDatasourceMoved($index)"
fxLayout="column" [dndDisableIf]="disabled"
fxLayoutAlign="center" dndEffectAllowed="move">
fxLayout.gt-sm="row" <div fxFlex fxLayout="row" fxLayoutAlign="start center">
fxLayoutAlign.gt-sm="start center"> <div style="width: 60px;">
<mat-form-field class="tb-datasource-type"> <button *ngIf="!disabled" mat-icon-button color="primary"
<mat-select formControlName="type"> class="handle"
<mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType"> style="min-width: 40px; margin: 0"
{{ datasourceTypesTranslations.get(datasourceType) | translate }} dndHandle
</mat-option> matTooltip="{{ 'action.drag' | translate }}"
</mat-select> matTooltipPosition="above">
</mat-form-field> <mat-icon>drag_handle</mat-icon>
<section class="tb-datasource" [ngSwitch]="datasourceControl.get('type').value"> </button>
<ng-template [ngSwitchCase]="datasourceType.function"> <span>{{$index + 1}}.</span>
<mat-form-field floatLabel="always" </div>
class="tb-datasource-name" style="min-width: 200px;"> <div class="mat-elevation-z4" fxFlex
<mat-label></mat-label> fxLayout="row"
<input matInput fxLayoutAlign="start center"
placeholder="{{ 'datasource.name' | translate }}" style="padding: 0 0 0 10px; margin: 5px;">
formControlName="name"> <section fxFlex
fxLayout="column"
fxLayoutAlign="center"
fxLayout.gt-sm="row"
fxLayoutAlign.gt-sm="start center">
<mat-form-field class="tb-datasource-type">
<mat-select [formControl]="datasourceControl.get('type')">
<mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
{{ datasourceTypesTranslations.get(datasourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field> </mat-form-field>
</ng-template> <section fxLayout="column" class="tb-datasource" [ngSwitch]="datasourceControl.get('type').value">
<ng-template [ngSwitchCase]="datasourceType.entity"> <ng-template [ngSwitchCase]="datasourceType.function">
<section fxLayout="column"> <mat-form-field floatLabel="always"
<tb-entity-alias-select class="tb-datasource-name" style="min-width: 200px;">
[showLabel]="true" <mat-label></mat-label>
[tbRequired]="datasourceControl.get('type').value === datasourceType.entity" <input matInput
[aliasController]="aliasController" placeholder="{{ 'datasource.label' | translate }}"
formControlName="entityAliasId" [formControl]="datasourceControl.get('name')">
[callbacks]="widgetConfigCallbacks"> </mat-form-field>
</tb-entity-alias-select> </ng-template>
<tb-filter-select <ng-template [ngSwitchCase]="datasourceControl.get('type').value === datasourceType.entity ||
[showLabel]="true" datasourceControl.get('type').value === datasourceType.entityCount ? datasourceControl.get('type').value : ''">
[aliasController]="aliasController" <tb-entity-alias-select
formControlName="filterId" [showLabel]="true"
[callbacks]="widgetConfigCallbacks"> [tbRequired]="true"
</tb-filter-select> [aliasController]="aliasController"
[formControl]="datasourceControl.get('entityAliasId')"
[callbacks]="widgetConfigCallbacks">
</tb-entity-alias-select>
<tb-filter-select
[showLabel]="true"
[aliasController]="aliasController"
[formControl]="datasourceControl.get('filterId')"
[callbacks]="widgetConfigCallbacks">
</tb-filter-select>
<mat-form-field *ngIf="datasourceControl.get('type').value === datasourceType.entityCount"
floatLabel="always"
class="tb-datasource-name no-border-top" style="min-width: 200px;">
<mat-label></mat-label>
<input matInput
placeholder="{{ 'datasource.label' | translate }}"
[formControl]="datasourceControl.get('name')">
</mat-form-field>
</ng-template>
</section> </section>
</ng-template> <tb-data-keys class="tb-data-keys" fxFlex
</section> [widgetType]="widgetType"
<tb-data-keys class="tb-data-keys" fxFlex [datasourceType]="datasourceControl.get('type').value"
[widgetType]="widgetType" [maxDataKeys]="modelValue?.typeParameters?.maxDataKeys"
[datasourceType]="datasourceControl.get('type').value" [optDataKeys]="modelValue?.typeParameters?.dataKeysOptional"
[maxDataKeys]="modelValue?.typeParameters?.maxDataKeys" [aliasController]="aliasController"
[optDataKeys]="modelValue?.typeParameters?.dataKeysOptional" [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
[aliasController]="aliasController" [callbacks]="widgetConfigCallbacks"
[datakeySettingsSchema]="modelValue?.dataKeySettingsSchema" [entityAliasId]="datasourceControl.get('entityAliasId').value"
[callbacks]="widgetConfigCallbacks" [formControl]="datasourceControl.get('dataKeys')">
[entityAliasId]="datasourceControl.get('entityAliasId').value" </tb-data-keys>
formControlName="dataKeys"> </section>
</tb-data-keys> <button [disabled]="isLoading$ | async"
</section> type="button"
<button [disabled]="isLoading$ | async" mat-icon-button color="primary"
type="button" style="min-width: 40px;"
mat-icon-button color="primary" (click)="removeDatasource($index)"
style="min-width: 40px;" matTooltip="{{ 'widget-config.remove-datasource' | translate }}"
(click)="removeDatasource($index)" matTooltipPosition="above">
matTooltip="{{ 'widget-config.remove-datasource' | translate }}" <mat-icon>close</mat-icon>
matTooltipPosition="above"> </button>
<mat-icon>close</mat-icon> </div>
</button> </div>
</div> </mat-list-item>
</div> </mat-list>
</div> </div>
</ng-template> </ng-template>
<div fxFlex fxLayout="row" fxLayoutAlign="start center"> <div fxFlex fxLayout="row" fxLayoutAlign="start center">

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

@ -23,10 +23,34 @@
.tb-advanced-widget-config { .tb-advanced-widget-config {
height: 100%; height: 100%;
} }
.tb-datasources {
.handle {
cursor: move;
}
.mat-list {
min-height: 68px;
padding-left: 0;
}
.mat-list-item {
height: auto;
min-height: 68px;
display: block;
&.dndDraggingSource {
display: none;
}
&.dndPlaceholder {
display: block;
background-color: #ddd;
}
}
}
.tb-datasource-type { .tb-datasource-type {
min-width: 110px; min-width: 120px;
@media #{$mat-gt-sm} { @media #{$mat-gt-sm} {
max-width: 110px; max-width: 120px;
} }
} }
.tb-datasource { .tb-datasource {
@ -70,13 +94,18 @@
white-space: normal; white-space: normal;
} }
.mat-expansion-panel { .mat-expansion-panel {
&.tb-datasources{ &.tb-datasources {
&.mat-expanded { &.mat-expanded {
overflow: visible; overflow: visible;
} }
.mat-expansion-panel-body{ .mat-expansion-panel-body{
padding: 0 12px 16px; padding: 0 12px 16px;
} }
.mat-list-base .mat-list-item {
.mat-list-item-content {
padding: 0;
}
}
} }
.mat-expansion-panel-content { .mat-expansion-panel-content {
font: inherit; font: inherit;
@ -110,6 +139,11 @@
} }
} }
} }
.tb-datasource-name.no-border-top {
.mat-form-field-infix {
border-top: 0;
}
}
} }
.tb-data-keys { .tb-data-keys {
@media #{$mat-gt-sm} { @media #{$mat-gt-sm} {

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

@ -40,7 +40,7 @@ import {
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { deepClone, isDefined, isObject } from '@app/core/utils'; import { deepClone, isDefined, isObject, isUndefined } from '@app/core/utils';
import { import {
alarmFields, alarmFields,
AlarmSearchStatus, AlarmSearchStatus,
@ -71,6 +71,7 @@ import { Filter, Filters } from '@shared/models/query/query.models';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips'; import { MatChipInputEvent } from '@angular/material/chips';
import { DndDropEvent } from 'ngx-drag-drop';
const emptySettingsSchema: JsonSchema = { const emptySettingsSchema: JsonSchema = {
type: 'object', type: 'object',
@ -141,7 +142,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
widgetType: widgetType; widgetType: widgetType;
datasourceType = DatasourceType; datasourceType = DatasourceType;
datasourceTypes: Array<DatasourceType>; datasourceTypes: Array<DatasourceType> = [];
datasourceTypesTranslations = datasourceTypeTranslationMap; datasourceTypesTranslations = datasourceTypeTranslationMap;
widgetConfigCallbacks: WidgetConfigCallbacks = { widgetConfigCallbacks: WidgetConfigCallbacks = {
@ -186,11 +187,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} }
ngOnInit(): void { ngOnInit(): void {
if (this.functionsOnly) {
this.datasourceTypes = [DatasourceType.function];
} else {
this.datasourceTypes = [DatasourceType.function, DatasourceType.entity];
}
this.dataSettings = this.fb.group({}); this.dataSettings = this.fb.group({});
this.targetDeviceSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({});
this.alarmSourceSettings = this.fb.group({}); this.alarmSourceSettings = this.fb.group({});
@ -295,6 +291,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} }
private buildForms() { private buildForms() {
if (this.functionsOnly) {
this.datasourceTypes = [DatasourceType.function];
} else {
this.datasourceTypes = [DatasourceType.function, DatasourceType.entity];
if (this.widgetType === widgetType.latest) {
this.datasourceTypes.push(DatasourceType.entityCount);
}
}
this.dataSettings = this.fb.group({}); this.dataSettings = this.fb.group({});
this.targetDeviceSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({});
this.alarmSourceSettings = this.fb.group({}); this.alarmSourceSettings = this.fb.group({});
@ -517,22 +521,28 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} }
private buildDatasourceForm(datasource?: Datasource): FormGroup { private buildDatasourceForm(datasource?: Datasource): FormGroup {
const dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional; let dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional
|| datasource?.type === DatasourceType.entityCount;
const datasourceFormGroup = this.fb.group( const datasourceFormGroup = this.fb.group(
{ {
type: [datasource ? datasource.type : null, [Validators.required]], type: [datasource ? datasource.type : null, [Validators.required]],
name: [datasource ? datasource.name : null, []], name: [datasource ? datasource.name : null, []],
entityAliasId: [datasource ? datasource.entityAliasId : null, entityAliasId: [datasource ? datasource.entityAliasId : null,
datasource && datasource.type === DatasourceType.entity ? [Validators.required] : []], datasource && (datasource.type === DatasourceType.entity ||
datasource.type === DatasourceType.entityCount) ? [Validators.required] : []],
filterId: [datasource ? datasource.filterId : null, []], filterId: [datasource ? datasource.filterId : null, []],
dataKeys: [datasource ? datasource.dataKeys : null, dataKeysRequired ? [Validators.required] : []] dataKeys: [datasource ? datasource.dataKeys : null, dataKeysRequired ? [Validators.required] : []]
} }
); );
datasourceFormGroup.get('type').valueChanges.subscribe((type: DatasourceType) => { datasourceFormGroup.get('type').valueChanges.subscribe((type: DatasourceType) => {
datasourceFormGroup.get('entityAliasId').setValidators( datasourceFormGroup.get('entityAliasId').setValidators(
type === DatasourceType.entity ? [Validators.required] : [] (type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : []
); );
dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional
|| type === DatasourceType.entityCount;
datasourceFormGroup.get('dataKeys').setValidators(dataKeysRequired ? [Validators.required] : []);
datasourceFormGroup.get('entityAliasId').updateValueAndValidity(); datasourceFormGroup.get('entityAliasId').updateValueAndValidity();
datasourceFormGroup.get('dataKeys').updateValueAndValidity();
}); });
return datasourceFormGroup; return datasourceFormGroup;
} }
@ -659,8 +669,22 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
return !!this.modelValue && !!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema; return !!this.modelValue && !!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema;
} }
public dndDatasourceMoved(index: number) {
this.datasourcesFormArray().removeAt(index);
}
public onDatasourceDrop(event: DndDropEvent) {
let index = event.index;
if (isUndefined(index)) {
index = this.datasourcesFormArray().length;
}
this.datasourcesFormArray().insert(index,
this.buildDatasourceForm(event.data)
);
}
public removeDatasource(index: number) { public removeDatasource(index: number) {
(this.dataSettings.get('datasources') as FormArray).removeAt(index); this.datasourcesFormArray().removeAt(index);
} }
public addDatasource() { public addDatasource() {
@ -673,8 +697,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
dataKeys: [] dataKeys: []
}; };
} }
const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray; this.datasourcesFormArray().push(this.buildDatasourceForm(newDatasource));
datasourcesFormArray.push(this.buildDatasourceForm(newDatasource));
} }
public generateDataKey(chip: any, type: DataKeyType): DataKey { public generateDataKey(chip: any, type: DataKeyType): DataKey {
@ -704,6 +727,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
if (!result.funcBody) { if (!result.funcBody) {
result.funcBody = 'return prevValue + 1;'; result.funcBody = 'return prevValue + 1;';
} }
} else if (type === DataKeyType.count) {
result.name = 'count';
} }
if (isDefined(this.modelValue.dataKeySettingsSchema.schema)) { if (isDefined(this.modelValue.dataKeySettingsSchema.schema)) {
result.settings = this.utils.generateObjectFromJsonSchema(this.modelValue.dataKeySettingsSchema.schema); result.settings = this.utils.generateObjectFromJsonSchema(this.modelValue.dataKeySettingsSchema.schema);

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

@ -84,6 +84,7 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
aliasController: IAliasController = new AliasController(this.utils, aliasController: IAliasController = new AliasController(this.utils,
this.entityService, this.entityService,
this.translate,
() => { return { () => { return {
getStateParams(): StateParams { getStateParams(): StateParams {
return {}; return {};

2
ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts

@ -124,6 +124,7 @@ class DraggableChip {
if (this.preventDrag) { if (this.preventDrag) {
event.preventDefault(); event.preventDefault();
} else { } else {
event.stopPropagation();
this.dragging = true; this.dragging = true;
globalDraggingChipListId = this.chipListElement.id; globalDraggingChipListId = this.chipListElement.id;
this.chipListElement.classList.add(draggingClassName); this.chipListElement.classList.add(draggingClassName);
@ -159,6 +160,7 @@ class DraggableChip {
} }
private onDragEnd(event: Event | any) { private onDragEnd(event: Event | any) {
event.stopPropagation();
this.dragging = false; this.dragging = false;
globalDraggingChipListId = null; globalDraggingChipListId = null;
this.chipListElement.classList.remove(draggingClassName); this.chipListElement.classList.remove(draggingClassName);

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

@ -36,7 +36,8 @@ export enum EntityKeyType {
TIME_SERIES = 'TIME_SERIES', TIME_SERIES = 'TIME_SERIES',
ENTITY_FIELD = 'ENTITY_FIELD', ENTITY_FIELD = 'ENTITY_FIELD',
ALARM_FIELD = 'ALARM_FIELD', ALARM_FIELD = 'ALARM_FIELD',
CONSTANT = 'CONSTANT' CONSTANT = 'CONSTANT',
COUNT = 'COUNT'
} }
export const entityKeyTypeTranslationMap = new Map<EntityKeyType, string>( export const entityKeyTypeTranslationMap = new Map<EntityKeyType, string>(
@ -61,6 +62,8 @@ export function entityKeyTypeToDataKeyType(entityKeyType: EntityKeyType): DataKe
return DataKeyType.entityField; return DataKeyType.entityField;
case EntityKeyType.ALARM_FIELD: case EntityKeyType.ALARM_FIELD:
return DataKeyType.alarm; return DataKeyType.alarm;
case EntityKeyType.COUNT:
return DataKeyType.count;
} }
} }
@ -76,6 +79,8 @@ export function dataKeyTypeToEntityKeyType(dataKeyType: DataKeyType): EntityKeyT
return EntityKeyType.ALARM_FIELD; return EntityKeyType.ALARM_FIELD;
case DataKeyType.entityField: case DataKeyType.entityField:
return EntityKeyType.ENTITY_FIELD; return EntityKeyType.ENTITY_FIELD;
case DataKeyType.count:
return EntityKeyType.COUNT;
} }
} }
@ -716,13 +721,13 @@ export const defaultEntityDataPageLink: EntityDataPageLink = createDefaultEntity
export interface EntityCountQuery { export interface EntityCountQuery {
entityFilter: EntityFilter; entityFilter: EntityFilter;
keyFilters?: Array<KeyFilter>;
} }
export interface AbstractDataQuery<T extends EntityDataPageLink> extends EntityCountQuery { export interface AbstractDataQuery<T extends EntityDataPageLink> extends EntityCountQuery {
pageLink: T; pageLink: T;
entityFields?: Array<EntityKey>; entityFields?: Array<EntityKey>;
latestValues?: Array<EntityKey>; latestValues?: Array<EntityKey>;
keyFilters?: Array<KeyFilter>;
} }
export interface EntityDataQuery extends AbstractDataQuery<EntityDataPageLink> { export interface EntityDataQuery extends AbstractDataQuery<EntityDataPageLink> {

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

@ -23,7 +23,7 @@ import { map } from 'rxjs/operators';
import { NgZone } from '@angular/core'; import { NgZone } from '@angular/core';
import { import {
AlarmData, AlarmData,
AlarmDataQuery, AlarmDataQuery, EntityCountQuery,
EntityData, EntityData,
EntityDataQuery, EntityDataQuery,
EntityKey, EntityKey,
@ -36,7 +36,8 @@ export enum DataKeyType {
attribute = 'attribute', attribute = 'attribute',
function = 'function', function = 'function',
alarm = 'alarm', alarm = 'alarm',
entityField = 'entityField' entityField = 'entityField',
count = 'count'
} }
export enum LatestTelemetry { export enum LatestTelemetry {
@ -181,6 +182,11 @@ export class EntityDataCmd implements WebsocketCmd {
} }
} }
export class EntityCountCmd implements WebsocketCmd {
cmdId: number;
query?: EntityCountQuery;
}
export class AlarmDataCmd implements WebsocketCmd { export class AlarmDataCmd implements WebsocketCmd {
cmdId: number; cmdId: number;
query?: AlarmDataQuery; query?: AlarmDataQuery;
@ -194,6 +200,10 @@ export class EntityDataUnsubscribeCmd implements WebsocketCmd {
cmdId: number; cmdId: number;
} }
export class EntityCountUnsubscribeCmd implements WebsocketCmd {
cmdId: number;
}
export class AlarmDataUnsubscribeCmd implements WebsocketCmd { export class AlarmDataUnsubscribeCmd implements WebsocketCmd {
cmdId: number; cmdId: number;
} }
@ -206,6 +216,8 @@ export class TelemetryPluginCmdsWrapper {
entityDataUnsubscribeCmds: Array<EntityDataUnsubscribeCmd>; entityDataUnsubscribeCmds: Array<EntityDataUnsubscribeCmd>;
alarmDataCmds: Array<AlarmDataCmd>; alarmDataCmds: Array<AlarmDataCmd>;
alarmDataUnsubscribeCmds: Array<AlarmDataUnsubscribeCmd>; alarmDataUnsubscribeCmds: Array<AlarmDataUnsubscribeCmd>;
entityCountCmds: Array<EntityCountCmd>;
entityCountUnsubscribeCmds: Array<EntityCountUnsubscribeCmd>;
constructor() { constructor() {
this.attrSubCmds = []; this.attrSubCmds = [];
@ -215,6 +227,8 @@ export class TelemetryPluginCmdsWrapper {
this.entityDataUnsubscribeCmds = []; this.entityDataUnsubscribeCmds = [];
this.alarmDataCmds = []; this.alarmDataCmds = [];
this.alarmDataUnsubscribeCmds = []; this.alarmDataUnsubscribeCmds = [];
this.entityCountCmds = [];
this.entityCountUnsubscribeCmds = [];
} }
public hasCommands(): boolean { public hasCommands(): boolean {
@ -224,7 +238,9 @@ export class TelemetryPluginCmdsWrapper {
this.entityDataCmds.length > 0 || this.entityDataCmds.length > 0 ||
this.entityDataUnsubscribeCmds.length > 0 || this.entityDataUnsubscribeCmds.length > 0 ||
this.alarmDataCmds.length > 0 || this.alarmDataCmds.length > 0 ||
this.alarmDataUnsubscribeCmds.length > 0; this.alarmDataUnsubscribeCmds.length > 0 ||
this.entityCountCmds.length > 0 ||
this.entityCountUnsubscribeCmds.length > 0;
} }
public clear() { public clear() {
@ -235,6 +251,8 @@ export class TelemetryPluginCmdsWrapper {
this.entityDataUnsubscribeCmds.length = 0; this.entityDataUnsubscribeCmds.length = 0;
this.alarmDataCmds.length = 0; this.alarmDataCmds.length = 0;
this.alarmDataUnsubscribeCmds.length = 0; this.alarmDataUnsubscribeCmds.length = 0;
this.entityCountCmds.length = 0;
this.entityCountUnsubscribeCmds.length = 0;
} }
public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper { public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper {
@ -253,6 +271,10 @@ export class TelemetryPluginCmdsWrapper {
preparedWrapper.alarmDataCmds = this.popCmds(this.alarmDataCmds, leftCount); preparedWrapper.alarmDataCmds = this.popCmds(this.alarmDataCmds, leftCount);
leftCount -= preparedWrapper.alarmDataCmds.length; leftCount -= preparedWrapper.alarmDataCmds.length;
preparedWrapper.alarmDataUnsubscribeCmds = this.popCmds(this.alarmDataUnsubscribeCmds, leftCount); preparedWrapper.alarmDataUnsubscribeCmds = this.popCmds(this.alarmDataUnsubscribeCmds, leftCount);
leftCount -= preparedWrapper.alarmDataUnsubscribeCmds.length;
preparedWrapper.entityCountCmds = this.popCmds(this.entityCountCmds, leftCount);
leftCount -= preparedWrapper.entityCountCmds.length;
preparedWrapper.entityCountUnsubscribeCmds = this.popCmds(this.entityCountUnsubscribeCmds, leftCount);
return preparedWrapper; return preparedWrapper;
} }
@ -280,40 +302,54 @@ export interface SubscriptionUpdateMsg extends SubscriptionDataHolder {
errorMsg: string; errorMsg: string;
} }
export enum DataUpdateType { export enum CmdUpdateType {
ENTITY_DATA = 'ENTITY_DATA', ENTITY_DATA = 'ENTITY_DATA',
ALARM_DATA = 'ALARM_DATA' ALARM_DATA = 'ALARM_DATA',
COUNT_DATA = 'COUNT_DATA'
} }
export interface DataUpdateMsg<T> { export interface CmdUpdateMsg {
cmdId: number; cmdId: number;
data?: PageData<T>;
update?: Array<T>;
errorCode: number; errorCode: number;
errorMsg: string; errorMsg: string;
dataUpdateType: DataUpdateType; cmdUpdateType: CmdUpdateType;
}
export interface DataUpdateMsg<T> extends CmdUpdateMsg {
data?: PageData<T>;
update?: Array<T>;
} }
export interface EntityDataUpdateMsg extends DataUpdateMsg<EntityData> { export interface EntityDataUpdateMsg extends DataUpdateMsg<EntityData> {
dataUpdateType: DataUpdateType.ENTITY_DATA; cmdUpdateType: CmdUpdateType.ENTITY_DATA;
} }
export interface AlarmDataUpdateMsg extends DataUpdateMsg<AlarmData> { export interface AlarmDataUpdateMsg extends DataUpdateMsg<AlarmData> {
dataUpdateType: DataUpdateType.ALARM_DATA; cmdUpdateType: CmdUpdateType.ALARM_DATA;
allowedEntities: number; allowedEntities: number;
totalEntities: number; totalEntities: number;
} }
export type WebsocketDataMsg = AlarmDataUpdateMsg | EntityDataUpdateMsg | SubscriptionUpdateMsg; export interface EntityCountUpdateMsg extends CmdUpdateMsg {
cmdUpdateType: CmdUpdateType.COUNT_DATA;
count: number;
}
export type WebsocketDataMsg = AlarmDataUpdateMsg | EntityDataUpdateMsg | EntityCountUpdateMsg | SubscriptionUpdateMsg;
export function isEntityDataUpdateMsg(message: WebsocketDataMsg): message is EntityDataUpdateMsg { export function isEntityDataUpdateMsg(message: WebsocketDataMsg): message is EntityDataUpdateMsg {
const updateMsg = (message as DataUpdateMsg<any>); const updateMsg = (message as CmdUpdateMsg);
return updateMsg.cmdId !== undefined && updateMsg.dataUpdateType === DataUpdateType.ENTITY_DATA; return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.ENTITY_DATA;
} }
export function isAlarmDataUpdateMsg(message: WebsocketDataMsg): message is AlarmDataUpdateMsg { export function isAlarmDataUpdateMsg(message: WebsocketDataMsg): message is AlarmDataUpdateMsg {
const updateMsg = (message as DataUpdateMsg<any>); const updateMsg = (message as CmdUpdateMsg);
return updateMsg.cmdId !== undefined && updateMsg.dataUpdateType === DataUpdateType.ALARM_DATA; return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.ALARM_DATA;
}
export function isEntityCountUpdateMsg(message: WebsocketDataMsg): message is EntityCountUpdateMsg {
const updateMsg = (message as CmdUpdateMsg);
return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.COUNT_DATA;
} }
export class SubscriptionUpdate implements SubscriptionUpdateMsg { export class SubscriptionUpdate implements SubscriptionUpdateMsg {
@ -365,21 +401,28 @@ export class SubscriptionUpdate implements SubscriptionUpdateMsg {
} }
} }
export class DataUpdate<T> implements DataUpdateMsg<T> { export class CmdUpdate implements CmdUpdateMsg {
cmdId: number; cmdId: number;
errorCode: number; errorCode: number;
errorMsg: string; errorMsg: string;
data?: PageData<T>; cmdUpdateType: CmdUpdateType;
update?: Array<T>;
dataUpdateType: DataUpdateType;
constructor(msg: DataUpdateMsg<T>) { constructor(msg: CmdUpdateMsg) {
this.cmdId = msg.cmdId; this.cmdId = msg.cmdId;
this.errorCode = msg.errorCode; this.errorCode = msg.errorCode;
this.errorMsg = msg.errorMsg; this.errorMsg = msg.errorMsg;
this.cmdUpdateType = msg.cmdUpdateType;
}
}
export class DataUpdate<T> extends CmdUpdate implements DataUpdateMsg<T> {
data?: PageData<T>;
update?: Array<T>;
constructor(msg: DataUpdateMsg<T>) {
super(msg);
this.data = msg.data; this.data = msg.data;
this.update = msg.update; this.update = msg.update;
this.dataUpdateType = msg.dataUpdateType;
} }
} }
@ -400,6 +443,15 @@ export class AlarmDataUpdate extends DataUpdate<AlarmData> {
} }
} }
export class EntityCountUpdate extends CmdUpdate {
count: number;
constructor(msg: EntityCountUpdateMsg) {
super(msg);
this.count = msg.count;
}
}
export interface TelemetryService { export interface TelemetryService {
subscribe(subscriber: TelemetrySubscriber); subscribe(subscriber: TelemetrySubscriber);
update(subscriber: TelemetrySubscriber); update(subscriber: TelemetrySubscriber);
@ -411,6 +463,7 @@ export class TelemetrySubscriber {
private dataSubject = new ReplaySubject<SubscriptionUpdate>(1); private dataSubject = new ReplaySubject<SubscriptionUpdate>(1);
private entityDataSubject = new ReplaySubject<EntityDataUpdate>(1); private entityDataSubject = new ReplaySubject<EntityDataUpdate>(1);
private alarmDataSubject = new ReplaySubject<AlarmDataUpdate>(1); private alarmDataSubject = new ReplaySubject<AlarmDataUpdate>(1);
private entityCountSubject = new ReplaySubject<EntityCountUpdate>(1);
private reconnectSubject = new Subject(); private reconnectSubject = new Subject();
private zone: NgZone; private zone: NgZone;
@ -420,6 +473,7 @@ export class TelemetrySubscriber {
public data$ = this.dataSubject.asObservable(); public data$ = this.dataSubject.asObservable();
public entityData$ = this.entityDataSubject.asObservable(); public entityData$ = this.entityDataSubject.asObservable();
public alarmData$ = this.alarmDataSubject.asObservable(); public alarmData$ = this.alarmDataSubject.asObservable();
public entityCount$ = this.entityCountSubject.asObservable();
public reconnect$ = this.reconnectSubject.asObservable(); public reconnect$ = this.reconnectSubject.asObservable();
public static createEntityAttributesSubscription(telemetryService: TelemetryService, public static createEntityAttributesSubscription(telemetryService: TelemetryService,
@ -464,6 +518,7 @@ export class TelemetrySubscriber {
this.dataSubject.complete(); this.dataSubject.complete();
this.entityDataSubject.complete(); this.entityDataSubject.complete();
this.alarmDataSubject.complete(); this.alarmDataSubject.complete();
this.entityCountSubject.complete();
this.reconnectSubject.complete(); this.reconnectSubject.complete();
} }
@ -513,6 +568,18 @@ export class TelemetrySubscriber {
} }
} }
public onEntityCount(message: EntityCountUpdate) {
if (this.zone) {
this.zone.run(
() => {
this.entityCountSubject.next(message);
}
);
} else {
this.entityCountSubject.next(message);
}
}
public onReconnected() { public onReconnected() {
this.reconnectSubject.next(); this.reconnectSubject.next();
} }

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

@ -246,13 +246,15 @@ export interface DataKey extends KeyInfo {
export enum DatasourceType { export enum DatasourceType {
function = 'function', function = 'function',
entity = 'entity' entity = 'entity',
entityCount = 'entityCount'
} }
export const datasourceTypeTranslationMap = new Map<DatasourceType, string>( export const datasourceTypeTranslationMap = new Map<DatasourceType, string>(
[ [
[ DatasourceType.function, 'function.function' ], [ DatasourceType.function, 'function.function' ],
[ DatasourceType.entity, 'entity.entity' ] [ DatasourceType.entity, 'entity.entity' ],
[ DatasourceType.entityCount, 'entity.entities-count' ]
] ]
); );

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

@ -52,6 +52,7 @@ import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { MatListModule } from '@angular/material/list';
import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core';
import { NgxDaterangepickerMd } from 'ngx-daterangepicker-material'; import { NgxDaterangepickerMd } from 'ngx-daterangepicker-material';
import { GridsterModule } from 'angular-gridster2'; import { GridsterModule } from 'angular-gridster2';
@ -132,6 +133,7 @@ import { TbJsonToStringDirective } from '@shared/components/directives/tb-json-t
import { JsonObjectEditDialogComponent } from '@shared/components/dialog/json-object-edit-dialog.component'; import { JsonObjectEditDialogComponent } from '@shared/components/dialog/json-object-edit-dialog.component';
import { HistorySelectorComponent } from './components/time/history-selector/history-selector.component'; import { HistorySelectorComponent } from './components/time/history-selector/history-selector.component';
import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component'; import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component';
import { DndModule } from 'ngx-drag-drop';
import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component';
import { ContactComponent } from '@shared/components/contact.component'; import { ContactComponent } from '@shared/components/contact.component';
import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component';
@ -259,6 +261,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
MatStepperModule, MatStepperModule,
MatAutocompleteModule, MatAutocompleteModule,
MatChipsModule, MatChipsModule,
MatListModule,
GridsterModule, GridsterModule,
ClipboardModule, ClipboardModule,
FlexLayoutModule.withConfig({addFlexToParent: false}), FlexLayoutModule.withConfig({addFlexToParent: false}),
@ -269,6 +272,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
HotkeyModule, HotkeyModule,
ColorPickerModule, ColorPickerModule,
NgxHmCarouselModule, NgxHmCarouselModule,
DndModule,
NgxFlowModule, NgxFlowModule,
NgxFlowchartModule NgxFlowchartModule
], ],
@ -348,6 +352,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
MatStepperModule, MatStepperModule,
MatAutocompleteModule, MatAutocompleteModule,
MatChipsModule, MatChipsModule,
MatListModule,
GridsterModule, GridsterModule,
ClipboardModule, ClipboardModule,
FlexLayoutModule, FlexLayoutModule,
@ -358,6 +363,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
HotkeyModule, HotkeyModule,
ColorPickerModule, ColorPickerModule,
NgxHmCarouselModule, NgxHmCarouselModule,
DndModule,
NgxFlowchartModule, NgxFlowchartModule,
ConfirmDialogComponent, ConfirmDialogComponent,
AlertDialogComponent, AlertDialogComponent,

2
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -800,6 +800,7 @@
"datasource": { "datasource": {
"type": "Datasource type", "type": "Datasource type",
"name": "Name", "name": "Name",
"label": "Label",
"add-datasource-prompt": "Please add datasource" "add-datasource-prompt": "Please add datasource"
}, },
"details": { "details": {
@ -1094,6 +1095,7 @@
"entity": { "entity": {
"entity": "Entity", "entity": "Entity",
"entities": "Entities", "entities": "Entities",
"entities-count": "Entities count",
"aliases": "Entity aliases", "aliases": "Entity aliases",
"entity-alias": "Entity alias", "entity-alias": "Entity alias",
"unable-delete-entity-alias-title": "Unable to delete entity alias", "unable-delete-entity-alias-title": "Unable to delete entity alias",

7
ui-ngx/yarn.lock

@ -6534,6 +6534,13 @@ ngx-daterangepicker-material@^4.0.1:
dependencies: dependencies:
tslib "^1.10.0" tslib "^1.10.0"
ngx-drag-drop@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ngx-drag-drop/-/ngx-drag-drop-2.0.0.tgz#65d970229964803726fb7b9af4aec24005c810c7"
integrity sha512-t+4/eiC8zaXKqU1ruNfFEfGs1GpMNwpffD0baopvZFKjQHCb5rhNqFilJ54wO4T0OwGp4/RnsVhlcxe1mX6UJg==
dependencies:
tslib "^1.9.0"
"ngx-flowchart@git://github.com/thingsboard/ngx-flowchart.git#master": "ngx-flowchart@git://github.com/thingsboard/ngx-flowchart.git#master":
version "0.0.0" version "0.0.0"
resolved "git://github.com/thingsboard/ngx-flowchart.git#078bfd2cedeeab412dee922e8066a19be6da7278" resolved "git://github.com/thingsboard/ngx-flowchart.git#078bfd2cedeeab412dee922e8066a19be6da7278"

Loading…
Cancel
Save