diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 13913d8d97..775a685ae8 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -68,6 +68,7 @@ "ngx-clipboard": "^13.0.1", "ngx-color-picker": "^10.1.0", "ngx-daterangepicker-material": "^4.0.1", + "ngx-drag-drop": "^2.0.0", "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master", "ngx-hm-carousel": "^2.0.0-rc.1", "ngx-sharebuttons": "^8.0.1", diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index 6ca208c506..5f7474a31b 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/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 { 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 { EntityService } from '@core/http/entity.service'; import { UtilsService } from '@core/services/utils.service'; @@ -27,6 +27,7 @@ import { defaultEntityDataPageLink, Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, updateDatasourceFromEntityInfo } from '@shared/models/query/query.models'; +import { TranslateService } from '@ngx-translate/core'; export class AliasController implements IAliasController { @@ -50,6 +51,7 @@ export class AliasController implements IAliasController { constructor(private utils: UtilsService, private entityService: EntityService, + private translate: TranslateService, private stateControllerHolder: StateControllerHolder, private origEntityAliases: EntityAliases, private origFilters: Filters) { @@ -241,7 +243,7 @@ export class AliasController implements IAliasController { private resolveDatasource(datasource: Datasource, forceFilter = false): Observable { const newDatasource = deepClone(datasource); - if (newDatasource.type === DatasourceType.entity) { + if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount) { if (newDatasource.filterId) { newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId); } @@ -329,16 +331,23 @@ export class AliasController implements IAliasController { return forkJoin(observables).pipe( map((result) => { let functionIndex = 0; + let entityCountIndex = 0; result.forEach((datasource) => { - if (datasource.type === DatasourceType.function) { + if (datasource.type === DatasourceType.function || datasource.type === DatasourceType.entityCount) { let name: string; if (datasource.name && datasource.name.length) { name = datasource.name; } else { - functionIndex++; - name = DatasourceType.function; - if (functionIndex > 1) { + if (datasource.type === DatasourceType.function) { + functionIndex++; + } else { + entityCountIndex++; + } + name = this.translate.instant(datasourceTypeTranslationMap.get(datasource.type)); + if (datasource.type === DatasourceType.function && functionIndex > 1) { name += ' ' + functionIndex; + } else if (datasource.type === DatasourceType.entityCount && entityCountIndex > 1) { + name += ' ' + entityCountIndex; } } datasource.name = name; diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index 6a8e21a389..79c505b07a 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -22,12 +22,14 @@ import { EntityFilter, EntityKey, EntityKeyType, - entityKeyTypeToDataKeyType, entityPageDataChanged, + entityKeyTypeToDataKeyType, + entityPageDataChanged, KeyFilter, TsValue } from '@shared/models/query/query.models'; import { DataKeyType, + EntityCountCmd, EntityDataCmd, SubscriptionData, SubscriptionDataHolder, @@ -42,6 +44,7 @@ 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 { Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { EntityId } from '@shared/models/id/entity-id'; import Timeout = NodeJS.Timeout; declare type DataKeyFunction = (time: number, prevValue: any) => any; @@ -82,6 +85,7 @@ export class EntityDataSubscription { private subscriber: TelemetrySubscriber; private dataCommand: EntityDataCmd; private subsCommand: EntityDataCmd; + private countCommand: EntityCountCmd; private attrFields: Array; private tsFields: Array; @@ -91,7 +95,7 @@ export class EntityDataSubscription { private pageData: PageData; private subsTw: SubscriptionTimewindow; private dataAggregators: Array; - private dataKeys: {[key: string]: Array | SubscriptionDataKey} = {} + private dataKeys: {[key: string]: Array | SubscriptionDataKey} = {}; private datasourceData: {[index: number]: {[key: string]: DataSetHolder}}; private datasourceOrigData: {[index: number]: {[key: string]: DataSetHolder}}; private entityIdToDataIndex: {[id: string]: number}; @@ -125,7 +129,8 @@ export class EntityDataSubscription { } } 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) { key = `${dataKey.name}_${dataKey.index}_${dataKey.type}`; } else { @@ -159,7 +164,7 @@ export class EntityDataSubscription { if (this.dataAggregators) { this.dataAggregators.forEach((aggregator) => { aggregator.destroy(); - }) + }); this.dataAggregators = null; } this.pageData = null; @@ -291,6 +296,65 @@ export class EntityDataSubscription { totalPages: 1 }; 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 = { + 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) { return of(null); @@ -347,7 +411,7 @@ export class EntityDataSubscription { limit: this.subsTw.aggregation.limit, agg: this.subsTw.aggregation.type, fetchLatestPreviousPoint: this.subsTw.aggregation.stateData - } + }; } } } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { @@ -381,7 +445,7 @@ export class EntityDataSubscription { if (this.dataAggregators) { this.dataAggregators.forEach((aggregator) => { aggregator.destroy(); - }) + }); } this.dataAggregators = []; this.resetData(); @@ -419,7 +483,8 @@ export class EntityDataSubscription { 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) { + if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount || + this.entityDataSubscriptionOptions.type === widgetType.timeseries) { const dataKeysList = dataKey as Array; for (let index = 0; index < dataKeysList.length; index++) { this.datasourceData[dataIndex][key + '_' + index] = { diff --git a/ui-ngx/src/app/core/api/entity-data.service.ts b/ui-ngx/src/app/core/api/entity-data.service.ts index c02f36a290..7b664417d0 100644 --- a/ui-ngx/src/app/core/api/entity-data.service.ts +++ b/ui-ngx/src/app/core/api/entity-data.service.ts @@ -135,11 +135,14 @@ export class EntityDataService { dataKeys: subscriptionDataKeys, type: subscriptionType }; - if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { + if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity || + entityDataSubscriptionOptions.datasourceType === DatasourceType.entityCount) { entityDataSubscriptionOptions.entityFilter = datasource.entityFilter; - entityDataSubscriptionOptions.pageLink = pageLink; entityDataSubscriptionOptions.keyFilters = keyFilters; entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; + if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { + entityDataSubscriptionOptions.pageLink = pageLink; + } } entityDataSubscriptionOptions.isPaginatedDataSubscription = isPaginatedDataSubscription; return entityDataSubscriptionOptions; diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 1118307d06..17a35b2305 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -1099,10 +1099,10 @@ export class EntityService { dataKeys: [] }; this.prepareEntityFilterFromSubscriptionInfo(datasource, subscriptionInfo); - } else if (subscriptionInfo.type === DatasourceType.function) { + } else if (subscriptionInfo.type === DatasourceType.function || subscriptionInfo.type === DatasourceType.entityCount) { datasource = { type: subscriptionInfo.type, - name: subscriptionInfo.name || DatasourceType.function, + name: subscriptionInfo.name || subscriptionInfo.type, dataKeys: [] }; } @@ -1119,6 +1119,10 @@ export class EntityService { if (subscriptionInfo.alarmFields) { 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; } diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index f3614e88d0..cabe78ff6e 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -93,7 +93,7 @@ export class ItemBufferService { if (widget.config.datasources) { for (let i = 0; i < widget.config.datasources.length; 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]; if (entityAlias) { aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias); @@ -119,7 +119,7 @@ export class ItemBufferService { if (widget.config.datasources) { for (let i = 0; i < widget.config.datasources.length; 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]; if (filter) { filtersInfo.datasourceFilters[i] = this.prepareFilterInfo(filter); diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 90b2989834..3c1c659e95 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -27,7 +27,8 @@ import { deleteNullProperties, guid, isDefined, - isDefinedAndNotNull, isString, + isDefinedAndNotNull, + isString, isUndefined } from '@core/utils'; import { WindowMessage } from '@shared/models/window-message.model'; diff --git a/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts b/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts index facdd0f60a..a3fc6eb767 100644 --- a/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts +++ b/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts @@ -16,18 +16,27 @@ import { Inject, Injectable, NgZone } from '@angular/core'; import { - AlarmDataCmd, AlarmDataUnsubscribeCmd, + AlarmDataCmd, + AlarmDataUnsubscribeCmd, AlarmDataUpdate, - AttributesSubscriptionCmd, EntityDataCmd, EntityDataUnsubscribeCmd, EntityDataUpdate, - GetHistoryCmd, isAlarmDataUpdateMsg, isEntityDataUpdateMsg, + AttributesSubscriptionCmd, + EntityCountCmd, EntityCountUnsubscribeCmd, + EntityCountUpdate, + EntityDataCmd, + EntityDataUnsubscribeCmd, + EntityDataUpdate, + GetHistoryCmd, + isAlarmDataUpdateMsg, + isEntityCountUpdateMsg, + isEntityDataUpdateMsg, SubscriptionCmd, SubscriptionUpdate, - SubscriptionUpdateMsg, TelemetryFeature, TelemetryPluginCmdsWrapper, TelemetryService, TelemetrySubscriber, - TimeseriesSubscriptionCmd, WebsocketDataMsg + TimeseriesSubscriptionCmd, + WebsocketDataMsg } from '@app/shared/models/telemetry/telemetry.models'; import { select, Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -111,6 +120,8 @@ export class TelemetryWebsocketService implements TelemetryService { this.cmdsWrapper.entityDataCmds.push(subscriptionCommand); } else if (subscriptionCommand instanceof AlarmDataCmd) { 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(); alarmDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; 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; if (cmdId) { @@ -236,7 +251,7 @@ export class TelemetryWebsocketService implements TelemetryService { { url: uri, openObserver: { - next: (e: Event) => { + next: () => { this.onOpen(); } }, @@ -294,6 +309,11 @@ export class TelemetryWebsocketService implements TelemetryService { if (subscriber) { 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) { subscriber = this.subscribersMap.get(message.subscriptionId); if (subscriber) { diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index 6456e6d6ed..7e83c67ec6 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/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.entityService, + this.translate, () => stateController, entitiAliases, filters); const dataKeyType: DataKeyType = this.attributeScope === LatestTelemetry.LATEST_TELEMETRY ? diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 5385913842..c154ef2867 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/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.entityService, + this.translate, () => this.dashboardCtx.stateController, this.dashboardConfiguration.entityAliases, this.dashboardConfiguration.filters); diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html index 45cc3a8087..fbc62010da 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html @@ -25,7 +25,7 @@ formControlName="name" required [matAutocomplete]="keyAutocomplete"> - - close + close - + @@ -128,7 +128,7 @@ {{ translate.get('entity.no-key-matching', {key: truncate.transform(searchText, true, 6, '...')}) | async }} - + entity.create-new-key diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts index c3f138cb80..96bd8c702f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts +++ b/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 { - AfterViewInit, - Component, - ElementRef, - forwardRef, - Input, - OnChanges, - OnInit, - SimpleChanges, - SkipSelf, - ViewChild + AfterViewInit, + Component, + ElementRef, + forwardRef, + Input, + OnChanges, + OnInit, + SimpleChanges, + SkipSelf, + ViewChild } from '@angular/core'; import { - ControlValueAccessor, - FormBuilder, - FormControl, - FormGroup, - FormGroupDirective, - NG_VALUE_ACCESSOR, - NgForm, - Validators + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NG_VALUE_ACCESSOR, + NgForm, + Validators } from '@angular/forms'; import { Observable, of } from 'rxjs'; 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 { MatDialog } from '@angular/material/dialog'; import { - DataKeyConfigDialogComponent, - DataKeyConfigDialogData + DataKeyConfigDialogComponent, + DataKeyConfigDialogData } from '@home/components/widget/data-key-config-dialog.component'; import { deepClone } from '@core/utils'; import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive'; @@ -94,8 +94,15 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie @Input() datasourceType: DatasourceType; + private maxDataKeysValue: number; + get maxDataKeys(): number { + return this.datasourceType === DatasourceType.entityCount ? 1 : this.maxDataKeysValue; + } + @Input() - maxDataKeys: number; + set maxDataKeys(value: number) { + this.maxDataKeysValue = value; + } @Input() optDataKeys: boolean; @@ -114,7 +121,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie private requiredValue: boolean; get required(): boolean { - return this.requiredValue || !this.optDataKeys; + return this.requiredValue || !this.optDataKeys || this.datasourceType === DatasourceType.entityCount; } @Input() set required(value: boolean) { @@ -210,8 +217,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie if (this.maxDataKeys !== null && this.maxDataKeys > -1) { if (this.datasourceType === DatasourceType.function) { 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}); + } else { + return ''; } } else { return ''; @@ -241,6 +250,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie private reset() { if (this.widgetType === widgetType.alarm) { this.keys = this.utils.getDefaultAlarmDataKeys(); + } else if (this.datasourceType === DatasourceType.entityCount) { + this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count)]; } else { this.keys = []; } @@ -426,7 +437,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; fetchObservable = of(targetKeysList); } else { - if (this.entityAliasId) { + if (this.datasourceType !== DatasourceType.entityCount && this.entityAliasId) { const dataKeyTypes = [DataKeyType.timeseries]; if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { dataKeyTypes.push(DataKeyType.attribute); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts index e79f6ef0e4..5fb11d025b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts +++ b/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 subscription: IWidgetSubscription; private datasources: Array; - private data: Array>; private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {}; private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {}; @@ -121,7 +120,6 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O this.widgetConfig = this.ctx.widgetConfig; this.subscription = this.ctx.defaultSubscription; this.datasources = this.subscription.datasources as Array; - this.data = this.subscription.dataPages[0].data; this.initializeConfig(); this.ctx.updateWidgetParams(); } @@ -252,12 +250,16 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O public loadNodes: LoadNodesCallback = (node, cb) => { if (node.id === '#') { const childNodes: HierarchyNavTreeNode[] = []; + let dataIndex = 0; 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)); } 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); /* (error) => { // TODO: let errorText = 'Failed to get relations!'; @@ -369,8 +371,8 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O } private datasourceToNode(datasource: HierarchyNodeDatasource, - data: DatasourceData[], - parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode { + data: DatasourceData[], + parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode { const node: HierarchyNavTreeNode = { id: (++this.nodeIdCounter) + '' }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts index 07ab91fe5d..74166b1742 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts +++ b/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) => { - 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) { result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name); } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index e47d96bebe..8b745c480f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -100,87 +100,115 @@
- +
- widget-config.datasource-type + widget-config.datasource-type widget-config.datasource-parameters
-
- {{$index + 1}}. -
-
- - - - {{ datasourceTypesTranslations.get(datasourceType) | translate }} - - - -
- - - - + + + + +
+
+ + {{$index + 1}}. +
+
+
+ + + + {{ datasourceTypesTranslations.get(datasourceType) | translate }} + + - - -
- - - - +
+ + + + + + + + + + + + + + + +
- -
- - -
- -
-
+ + +
+ +
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss index 39182661f1..6e1e1cabdf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss @@ -23,10 +23,34 @@ .tb-advanced-widget-config { 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 { - min-width: 110px; + min-width: 120px; @media #{$mat-gt-sm} { - max-width: 110px; + max-width: 120px; } } .tb-datasource { @@ -70,13 +94,18 @@ white-space: normal; } .mat-expansion-panel { - &.tb-datasources{ + &.tb-datasources { &.mat-expanded { overflow: visible; } .mat-expansion-panel-body{ padding: 0 12px 16px; } + .mat-list-base .mat-list-item { + .mat-list-item-content { + padding: 0; + } + } } .mat-expansion-panel-content { font: inherit; @@ -110,6 +139,11 @@ } } } + .tb-datasource-name.no-border-top { + .mat-form-field-infix { + border-top: 0; + } + } } .tb-data-keys { @media #{$mat-gt-sm} { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 05c6264a92..523913afe4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -40,7 +40,7 @@ import { Validators } from '@angular/forms'; 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 { alarmFields, 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 { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { MatChipInputEvent } from '@angular/material/chips'; +import { DndDropEvent } from 'ngx-drag-drop'; const emptySettingsSchema: JsonSchema = { type: 'object', @@ -141,7 +142,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont widgetType: widgetType; datasourceType = DatasourceType; - datasourceTypes: Array; + datasourceTypes: Array = []; datasourceTypesTranslations = datasourceTypeTranslationMap; widgetConfigCallbacks: WidgetConfigCallbacks = { @@ -186,11 +187,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } ngOnInit(): void { - if (this.functionsOnly) { - this.datasourceTypes = [DatasourceType.function]; - } else { - this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; - } this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); this.alarmSourceSettings = this.fb.group({}); @@ -295,6 +291,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } 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.targetDeviceSettings = 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 { - 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( { type: [datasource ? datasource.type : null, [Validators.required]], name: [datasource ? datasource.name : 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, []], dataKeys: [datasource ? datasource.dataKeys : null, dataKeysRequired ? [Validators.required] : []] } ); datasourceFormGroup.get('type').valueChanges.subscribe((type: DatasourceType) => { 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('dataKeys').updateValueAndValidity(); }); return datasourceFormGroup; } @@ -659,8 +669,22 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont 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) { - (this.dataSettings.get('datasources') as FormArray).removeAt(index); + this.datasourcesFormArray().removeAt(index); } public addDatasource() { @@ -673,8 +697,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont dataKeys: [] }; } - const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray; - datasourcesFormArray.push(this.buildDatasourceForm(newDatasource)); + this.datasourcesFormArray().push(this.buildDatasourceForm(newDatasource)); } public generateDataKey(chip: any, type: DataKeyType): DataKey { @@ -704,6 +727,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont if (!result.funcBody) { result.funcBody = 'return prevValue + 1;'; } + } else if (type === DataKeyType.count) { + result.name = 'count'; } if (isDefined(this.modelValue.dataKeySettingsSchema.schema)) { result.settings = this.utils.generateObjectFromJsonSchema(this.modelValue.dataKeySettingsSchema.schema); diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts index 8e99a14016..0be34d5f39 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts +++ b/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, this.entityService, + this.translate, () => { return { getStateParams(): StateParams { return {}; diff --git a/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts b/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts index 246f38b87a..f9ada15d64 100644 --- a/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts +++ b/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts @@ -124,6 +124,7 @@ class DraggableChip { if (this.preventDrag) { event.preventDefault(); } else { + event.stopPropagation(); this.dragging = true; globalDraggingChipListId = this.chipListElement.id; this.chipListElement.classList.add(draggingClassName); @@ -159,6 +160,7 @@ class DraggableChip { } private onDragEnd(event: Event | any) { + event.stopPropagation(); this.dragging = false; globalDraggingChipListId = null; this.chipListElement.classList.remove(draggingClassName); diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index b8dc2564bc..dc21f70907 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -36,7 +36,8 @@ export enum EntityKeyType { TIME_SERIES = 'TIME_SERIES', ENTITY_FIELD = 'ENTITY_FIELD', ALARM_FIELD = 'ALARM_FIELD', - CONSTANT = 'CONSTANT' + CONSTANT = 'CONSTANT', + COUNT = 'COUNT' } export const entityKeyTypeTranslationMap = new Map( @@ -61,6 +62,8 @@ export function entityKeyTypeToDataKeyType(entityKeyType: EntityKeyType): DataKe return DataKeyType.entityField; case EntityKeyType.ALARM_FIELD: return DataKeyType.alarm; + case EntityKeyType.COUNT: + return DataKeyType.count; } } @@ -76,6 +79,8 @@ export function dataKeyTypeToEntityKeyType(dataKeyType: DataKeyType): EntityKeyT return EntityKeyType.ALARM_FIELD; case DataKeyType.entityField: return EntityKeyType.ENTITY_FIELD; + case DataKeyType.count: + return EntityKeyType.COUNT; } } @@ -716,13 +721,13 @@ export const defaultEntityDataPageLink: EntityDataPageLink = createDefaultEntity export interface EntityCountQuery { entityFilter: EntityFilter; + keyFilters?: Array; } export interface AbstractDataQuery extends EntityCountQuery { pageLink: T; entityFields?: Array; latestValues?: Array; - keyFilters?: Array; } export interface EntityDataQuery extends AbstractDataQuery { diff --git a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts index 1e77285667..cfbde53233 100644 --- a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts +++ b/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 { AlarmData, - AlarmDataQuery, + AlarmDataQuery, EntityCountQuery, EntityData, EntityDataQuery, EntityKey, @@ -36,7 +36,8 @@ export enum DataKeyType { attribute = 'attribute', function = 'function', alarm = 'alarm', - entityField = 'entityField' + entityField = 'entityField', + count = 'count' } 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 { cmdId: number; query?: AlarmDataQuery; @@ -194,6 +200,10 @@ export class EntityDataUnsubscribeCmd implements WebsocketCmd { cmdId: number; } +export class EntityCountUnsubscribeCmd implements WebsocketCmd { + cmdId: number; +} + export class AlarmDataUnsubscribeCmd implements WebsocketCmd { cmdId: number; } @@ -206,6 +216,8 @@ export class TelemetryPluginCmdsWrapper { entityDataUnsubscribeCmds: Array; alarmDataCmds: Array; alarmDataUnsubscribeCmds: Array; + entityCountCmds: Array; + entityCountUnsubscribeCmds: Array; constructor() { this.attrSubCmds = []; @@ -215,6 +227,8 @@ export class TelemetryPluginCmdsWrapper { this.entityDataUnsubscribeCmds = []; this.alarmDataCmds = []; this.alarmDataUnsubscribeCmds = []; + this.entityCountCmds = []; + this.entityCountUnsubscribeCmds = []; } public hasCommands(): boolean { @@ -224,7 +238,9 @@ export class TelemetryPluginCmdsWrapper { this.entityDataCmds.length > 0 || this.entityDataUnsubscribeCmds.length > 0 || this.alarmDataCmds.length > 0 || - this.alarmDataUnsubscribeCmds.length > 0; + this.alarmDataUnsubscribeCmds.length > 0 || + this.entityCountCmds.length > 0 || + this.entityCountUnsubscribeCmds.length > 0; } public clear() { @@ -235,6 +251,8 @@ export class TelemetryPluginCmdsWrapper { this.entityDataUnsubscribeCmds.length = 0; this.alarmDataCmds.length = 0; this.alarmDataUnsubscribeCmds.length = 0; + this.entityCountCmds.length = 0; + this.entityCountUnsubscribeCmds.length = 0; } public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper { @@ -253,6 +271,10 @@ export class TelemetryPluginCmdsWrapper { preparedWrapper.alarmDataCmds = this.popCmds(this.alarmDataCmds, leftCount); leftCount -= preparedWrapper.alarmDataCmds.length; 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; } @@ -280,40 +302,54 @@ export interface SubscriptionUpdateMsg extends SubscriptionDataHolder { errorMsg: string; } -export enum DataUpdateType { +export enum CmdUpdateType { ENTITY_DATA = 'ENTITY_DATA', - ALARM_DATA = 'ALARM_DATA' + ALARM_DATA = 'ALARM_DATA', + COUNT_DATA = 'COUNT_DATA' } -export interface DataUpdateMsg { +export interface CmdUpdateMsg { cmdId: number; - data?: PageData; - update?: Array; errorCode: number; errorMsg: string; - dataUpdateType: DataUpdateType; + cmdUpdateType: CmdUpdateType; +} + +export interface DataUpdateMsg extends CmdUpdateMsg { + data?: PageData; + update?: Array; } export interface EntityDataUpdateMsg extends DataUpdateMsg { - dataUpdateType: DataUpdateType.ENTITY_DATA; + cmdUpdateType: CmdUpdateType.ENTITY_DATA; } export interface AlarmDataUpdateMsg extends DataUpdateMsg { - dataUpdateType: DataUpdateType.ALARM_DATA; + cmdUpdateType: CmdUpdateType.ALARM_DATA; allowedEntities: 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 { - const updateMsg = (message as DataUpdateMsg); - return updateMsg.cmdId !== undefined && updateMsg.dataUpdateType === DataUpdateType.ENTITY_DATA; + const updateMsg = (message as CmdUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.ENTITY_DATA; } export function isAlarmDataUpdateMsg(message: WebsocketDataMsg): message is AlarmDataUpdateMsg { - const updateMsg = (message as DataUpdateMsg); - return updateMsg.cmdId !== undefined && updateMsg.dataUpdateType === DataUpdateType.ALARM_DATA; + const updateMsg = (message as CmdUpdateMsg); + 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 { @@ -365,21 +401,28 @@ export class SubscriptionUpdate implements SubscriptionUpdateMsg { } } -export class DataUpdate implements DataUpdateMsg { +export class CmdUpdate implements CmdUpdateMsg { cmdId: number; errorCode: number; errorMsg: string; - data?: PageData; - update?: Array; - dataUpdateType: DataUpdateType; + cmdUpdateType: CmdUpdateType; - constructor(msg: DataUpdateMsg) { + constructor(msg: CmdUpdateMsg) { this.cmdId = msg.cmdId; this.errorCode = msg.errorCode; this.errorMsg = msg.errorMsg; + this.cmdUpdateType = msg.cmdUpdateType; + } +} + +export class DataUpdate extends CmdUpdate implements DataUpdateMsg { + data?: PageData; + update?: Array; + + constructor(msg: DataUpdateMsg) { + super(msg); this.data = msg.data; this.update = msg.update; - this.dataUpdateType = msg.dataUpdateType; } } @@ -400,6 +443,15 @@ export class AlarmDataUpdate extends DataUpdate { } } +export class EntityCountUpdate extends CmdUpdate { + count: number; + + constructor(msg: EntityCountUpdateMsg) { + super(msg); + this.count = msg.count; + } +} + export interface TelemetryService { subscribe(subscriber: TelemetrySubscriber); update(subscriber: TelemetrySubscriber); @@ -411,6 +463,7 @@ export class TelemetrySubscriber { private dataSubject = new ReplaySubject(1); private entityDataSubject = new ReplaySubject(1); private alarmDataSubject = new ReplaySubject(1); + private entityCountSubject = new ReplaySubject(1); private reconnectSubject = new Subject(); private zone: NgZone; @@ -420,6 +473,7 @@ export class TelemetrySubscriber { public data$ = this.dataSubject.asObservable(); public entityData$ = this.entityDataSubject.asObservable(); public alarmData$ = this.alarmDataSubject.asObservable(); + public entityCount$ = this.entityCountSubject.asObservable(); public reconnect$ = this.reconnectSubject.asObservable(); public static createEntityAttributesSubscription(telemetryService: TelemetryService, @@ -464,6 +518,7 @@ export class TelemetrySubscriber { this.dataSubject.complete(); this.entityDataSubject.complete(); this.alarmDataSubject.complete(); + this.entityCountSubject.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() { this.reconnectSubject.next(); } diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 179a6ff47b..55e6b88e34 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -246,13 +246,15 @@ export interface DataKey extends KeyInfo { export enum DatasourceType { function = 'function', - entity = 'entity' + entity = 'entity', + entityCount = 'entityCount' } export const datasourceTypeTranslationMap = new Map( [ [ DatasourceType.function, 'function.function' ], - [ DatasourceType.entity, 'entity.entity' ] + [ DatasourceType.entity, 'entity.entity' ], + [ DatasourceType.entityCount, 'entity.entities-count' ] ] ); diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index ba56ad72fc..a142a48e90 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/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 { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatListModule } from '@angular/material/list'; import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; import { NgxDaterangepickerMd } from 'ngx-daterangepicker-material'; 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 { HistorySelectorComponent } from './components/time/history-selector/history-selector.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 { ContactComponent } from '@shared/components/contact.component'; import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; @@ -259,6 +261,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select MatStepperModule, MatAutocompleteModule, MatChipsModule, + MatListModule, GridsterModule, ClipboardModule, FlexLayoutModule.withConfig({addFlexToParent: false}), @@ -269,6 +272,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select HotkeyModule, ColorPickerModule, NgxHmCarouselModule, + DndModule, NgxFlowModule, NgxFlowchartModule ], @@ -348,6 +352,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select MatStepperModule, MatAutocompleteModule, MatChipsModule, + MatListModule, GridsterModule, ClipboardModule, FlexLayoutModule, @@ -358,6 +363,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select HotkeyModule, ColorPickerModule, NgxHmCarouselModule, + DndModule, NgxFlowchartModule, ConfirmDialogComponent, AlertDialogComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 216ae2c1b7..50fbe58583 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -800,6 +800,7 @@ "datasource": { "type": "Datasource type", "name": "Name", + "label": "Label", "add-datasource-prompt": "Please add datasource" }, "details": { @@ -1094,6 +1095,7 @@ "entity": { "entity": "Entity", "entities": "Entities", + "entities-count": "Entities count", "aliases": "Entity aliases", "entity-alias": "Entity alias", "unable-delete-entity-alias-title": "Unable to delete entity alias", diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index b993a8dbd4..b1c5167ac9 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -6534,6 +6534,13 @@ ngx-daterangepicker-material@^4.0.1: dependencies: 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": version "0.0.0" resolved "git://github.com/thingsboard/ngx-flowchart.git#078bfd2cedeeab412dee922e8066a19be6da7278"