diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts index eed742c0c4..1031f7d6cc 100644 --- a/ui-ngx/src/app/app.component.ts +++ b/ui-ngx/src/app/app.component.ts @@ -57,6 +57,14 @@ export class AppComponent implements OnInit { '0,10 0 0,1 2,12A10,10 0 0,1 12,2Z" />' ) ); + this.matIconRegistry.addSvgIconLiteral( + 'alpha-e-circle-outline', + this.domSanitizer.bypassSecurityTrustHtml( + '' + ) + ); this.storageService.testLocalStorage(); diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 28c3831266..8bc293ccf1 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -50,6 +50,8 @@ import { AlarmSourceListener } from '@core/http/alarm.service'; import { DatasourceListener } from '@core/api/datasource.service'; import * as deepEqual from 'deep-equal'; import { EntityId } from '@app/shared/models/id/entity-id'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { entityFields } from '@shared/models/entity.models'; export class WidgetSubscription implements IWidgetSubscription { @@ -342,6 +344,12 @@ export class WidgetSubscription implements IWidgetSubscription { dataKey, data: [] }; + if (dataKey.type === DataKeyType.entityField && datasource.entity) { + const propName = entityFields[dataKey.name] ? entityFields[dataKey.name].value : dataKey.name; + if (datasource.entity[propName]) { + datasourceData.data.push([Date.now(), datasource.entity[propName]]); + } + } this.data.push(datasourceData); this.hiddenData.push({data: []}); if (this.displayLegend) { @@ -682,8 +690,15 @@ export class WidgetSubscription implements IWidgetSubscription { }, datasourceIndex: index }; + + let entityFieldKey = false; + for (let a = 0; a < datasource.dataKeys.length; a++) { - this.data[index + a].data = []; + if (datasource.dataKeys[a].type !== DataKeyType.entityField) { + this.data[index + a].data = []; + } else { + entityFieldKey = true; + } } index += datasource.dataKeys.length; this.datasourceListeners.push(listener); @@ -691,7 +706,7 @@ export class WidgetSubscription implements IWidgetSubscription { if (datasource.dataKeys.length) { this.ctx.datasourceService.subscribeToDatasource(listener); } - if (datasource.unresolvedStateEntity || + if (datasource.unresolvedStateEntity || entityFieldKey || !datasource.dataKeys.length || (datasource.type === DatasourceType.entity && !datasource.entityId) ) { diff --git a/ui-ngx/src/app/core/http/attribute.service.ts b/ui-ngx/src/app/core/http/attribute.service.ts index 05cc3f3665..663c7b5749 100644 --- a/ui-ngx/src/app/core/http/attribute.service.ts +++ b/ui-ngx/src/app/core/http/attribute.service.ts @@ -45,11 +45,11 @@ export class AttributeService { defaultHttpOptionsFromConfig(config)); } - public deleteEntityTimeseries(entityId: EntityId, timeseries: Array, + public deleteEntityTimeseries(entityId: EntityId, timeseries: Array, deleteAllDataForKeys = false, config?: RequestConfig): Observable { const keys = timeseries.map(attribute => attribute.key).join(','); return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` + - `?keys=${keys}`, + `?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`, defaultHttpOptionsFromConfig(config)); } @@ -93,7 +93,7 @@ export class AttributeService { }); let deleteEntityTimeseriesObservable: Observable; if (deleteTimeseries.length) { - deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, config); + deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, config); } else { deleteEntityTimeseriesObservable = of(null); } diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index 3e325039c3..9bc2e74ecc 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -20,7 +20,13 @@ import { Observable, ReplaySubject } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; -import { Device, DeviceCredentials, DeviceInfo, DeviceSearchQuery } from '@app/shared/models/device.models'; +import { + ClaimRequest, ClaimResult, + Device, + DeviceCredentials, + DeviceInfo, + DeviceSearchQuery +} from '@app/shared/models/device.models'; import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { AuthService } from '@core/auth/auth.service'; @@ -127,4 +133,13 @@ export class DeviceService { return this.http.get(`/api/tenant/devices?deviceName=${deviceName}`, defaultHttpOptionsFromConfig(config)); } + public claimDevice(deviceName: string, claimRequest: ClaimRequest, + config?: RequestConfig): Observable { + return this.http.post(`api/customer/device/${deviceName}/claim`, claimRequest, defaultHttpOptionsFromConfig(config)); + } + + public unclaimDevice(deviceName: string, config?: RequestConfig) { + return this.http.delete(`/api/customer/device/${deviceName}/claim`, defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index e8d7ff0234..442cd22aab 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -44,7 +44,7 @@ import { AliasInfo, StateParams, SubscriptionInfo } from '@core/api/widget-api.m import { Datasource, DatasourceType, KeyInfo } from '@app/shared/models/widget.models'; import { UtilsService } from '@core/services/utils.service'; import { AliasFilterType, EntityAlias, EntityAliasFilter, EntityAliasFilterResult } from '@shared/models/alias.models'; -import { EntityInfo, ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models'; +import { entityFields, EntityInfo, ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models'; import { EntityRelationInfo, EntityRelationsQuery, @@ -503,8 +503,50 @@ export class EntityService { return entityTypes; } + private getEntityFieldKeys (entityType: EntityType, searchText: string): Array { + const entityFieldKeys: string[] = []; + const query = searchText.toLowerCase(); + switch(entityType) { + case EntityType.USER: + entityFieldKeys.push(entityFields.name.keyName); + entityFieldKeys.push(entityFields.email.keyName); + entityFieldKeys.push(entityFields.firstName.keyName); + entityFieldKeys.push(entityFields.lastName.keyName); + break; + case EntityType.TENANT: + case EntityType.CUSTOMER: + entityFieldKeys.push(entityFields.title.keyName); + entityFieldKeys.push(entityFields.email.keyName); + entityFieldKeys.push(entityFields.country.keyName); + entityFieldKeys.push(entityFields.state.keyName); + entityFieldKeys.push(entityFields.city.keyName); + entityFieldKeys.push(entityFields.address.keyName); + entityFieldKeys.push(entityFields.address2.keyName); + entityFieldKeys.push(entityFields.zip.keyName); + entityFieldKeys.push(entityFields.phone.keyName); + break; + case EntityType.ENTITY_VIEW: + entityFieldKeys.push(entityFields.name.keyName); + entityFieldKeys.push(entityFields.type.keyName); + break; + case EntityType.DEVICE: + case EntityType.ASSET: + entityFieldKeys.push(entityFields.name.keyName); + entityFieldKeys.push(entityFields.type.keyName); + entityFieldKeys.push(entityFields.label.keyName); + break; + case EntityType.DASHBOARD: + entityFieldKeys.push(entityFields.title.keyName); + break; + } + return query ? entityFieldKeys.filter((entityField) => entityField.toLowerCase().indexOf(query) === 0) : entityFieldKeys; + } + public getEntityKeys(entityId: EntityId, query: string, type: DataKeyType, config?: RequestConfig): Observable> { + if (type === DataKeyType.entityField) { + return of(this.getEntityFieldKeys(entityId.entityType as EntityType, query)); + } let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/keys/`; if (type === DataKeyType.timeseries) { url += 'timeseries'; @@ -588,7 +630,8 @@ export class EntityService { if (filter.stateEntityParamName && filter.stateEntityParamName.length) { result.entityParamName = filter.stateEntityParamName; } - const stateEntityId = this.getStateEntityId(filter, stateParams); + const stateEntityInfo = this.getStateEntityInfo(filter, stateParams); + const stateEntityId = stateEntityInfo.entityId; switch (filter.type) { case AliasFilterType.singleEntity: const aliasEntityId = this.resolveAliasEntityId(filter.singleEntity.entityType, filter.singleEntity.id); @@ -697,7 +740,8 @@ export class EntityService { parameters: { rootId: relationQueryRootEntityId.id, rootType: relationQueryRootEntityId.entityType as EntityType, - direction: filter.direction + direction: filter.direction, + fetchLastLevelOnly: filter.fetchLastLevelOnly }, filters: filter.filters }; @@ -741,10 +785,12 @@ export class EntityService { parameters: { rootId: searchQueryRootEntityId.id, rootType: searchQueryRootEntityId.entityType as EntityType, - direction: filter.direction + direction: filter.direction, + fetchLastLevelOnly: filter.fetchLastLevelOnly }, relationType: filter.relationType }; + searchQuery.parameters.maxLevel = filter.maxLevel && filter.maxLevel > 0 ? filter.maxLevel : -1; let findByQueryObservable: Observable>>; if (filter.type === AliasFilterType.assetSearchQuery) { const assetSearchQuery = searchQuery as AssetSearchQuery; @@ -988,8 +1034,8 @@ export class EntityService { ); } - private getStateEntityId(filter: EntityAliasFilter, stateParams: StateParams): EntityId { - let entityId = null; + private getStateEntityInfo(filter: EntityAliasFilter, stateParams: StateParams): {entityId: EntityId} { + let entityId: EntityId = null; if (stateParams) { if (filter.stateEntityParamName && filter.stateEntityParamName.length) { if (stateParams[filter.stateEntityParamName]) { @@ -1005,7 +1051,7 @@ export class EntityService { if (entityId) { entityId = this.resolveAliasEntityId(entityId.entityType, entityId.id); } - return entityId; + return {entityId}; } private resolveAliasEntityId(entityType: EntityType | AliasEntityType, id: string): EntityId { diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html index 05247b5e04..6670b17214 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html @@ -133,6 +133,14 @@ +
+
+ + + +
+
relation.direction @@ -191,6 +199,14 @@
+
+
+ + + +
+
relation.direction diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.scss b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.scss index 267e0a5773..01e8dc7cc8 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.scss +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.scss @@ -25,6 +25,7 @@ .tb-root-state-entity-switch { padding-left: 10px; + padding-bottom: 10px; .root-state-entity-switch { margin: 0; diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts index ecaaad5fd2..600ce2d6da 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts @@ -165,6 +165,7 @@ export class EntityFilterComponent implements ControlValueAccessor, OnInit { rootEntity: [filter ? filter.rootEntity : null, (filter && filter.rootStateEntity) ? [] : [Validators.required]], direction: [filter ? filter.direction : EntitySearchDirection.FROM, [Validators.required]], maxLevel: [filter ? filter.maxLevel : 1, []], + fetchLastLevelOnly: [filter ? filter.fetchLastLevelOnly : false, []] }); this.filterFormGroup.get('rootStateEntity').valueChanges.subscribe((rootStateEntity: boolean) => { this.filterFormGroup.get('rootEntity').setValidators(rootStateEntity ? [] : [Validators.required]); diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html index c8311620e0..8b15c59196 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html @@ -38,6 +38,12 @@ + + + @@ -87,6 +93,12 @@ + + + @@ -114,15 +126,22 @@ - - + + + + - timeline - +
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 e4b6a32637..29482e63b9 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 @@ -420,6 +420,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie const dataKeyTypes = [DataKeyType.timeseries]; if (this.widgetType === widgetType.latest) { dataKeyTypes.push(DataKeyType.attribute); + dataKeyTypes.push(DataKeyType.entityField); } fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, this.searchText, dataKeyTypes); } else { 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 8446ee0723..c9735fd364 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 @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectionStrategy, Component, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -22,12 +22,11 @@ import { DataKey, Datasource, DatasourceType, - datasourceTypeTranslationMap, defaultLegendConfig, - WidgetActionDescriptor, + datasourceTypeTranslationMap, + defaultLegendConfig, widgetType } from '@shared/models/widget.models'; import { - AbstractControl, ControlValueAccessor, FormArray, FormBuilder, @@ -58,7 +57,8 @@ import { MatDialog } from '@angular/material/dialog'; import { EntityService } from '@core/http/entity.service'; import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; import { WidgetActionsData } from './action/manage-widget-actions.component.models'; -import { Dashboard, DashboardState } from '@shared/models/dashboard.models'; +import { DashboardState } from '@shared/models/dashboard.models'; +import { entityFields } from '@shared/models/entity.models'; const emptySettingsSchema = { type: 'object', @@ -621,10 +621,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont return chip; } else { let label: string = chip; - if (type === DataKeyType.alarm) { - const alarmField = alarmFields[label]; - if (alarmField) { - label = this.translate.instant(alarmField.name); + if (type === DataKeyType.alarm || type === DataKeyType.entityField) { + const keyField = type === DataKeyType.alarm ? alarmFields[label] : entityFields[chip];; + if (keyField) { + label = this.translate.instant(keyField.name); } } label = this.genNextLabel(label); diff --git a/ui-ngx/src/app/shared/models/alias.models.ts b/ui-ngx/src/app/shared/models/alias.models.ts index 14f9193952..50d26f10a0 100644 --- a/ui-ngx/src/app/shared/models/alias.models.ts +++ b/ui-ngx/src/app/shared/models/alias.models.ts @@ -91,6 +91,7 @@ export interface RelationsQueryFilter { direction?: EntitySearchDirection; filters?: Array; maxLevel?: number; + fetchLastLevelOnly?: boolean; } export interface EntitySearchQueryFilter { @@ -100,6 +101,8 @@ export interface EntitySearchQueryFilter { rootEntity?: EntityId; relationType?: string; direction?: EntitySearchDirection; + maxLevel?: number; + fetchLastLevelOnly?: boolean; } export interface AssetSearchQueryFilter extends EntitySearchQueryFilter { diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index ef527eb33c..7378fc430a 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -57,3 +57,18 @@ export interface DeviceCredentials extends BaseData { export interface DeviceSearchQuery extends EntitySearchQuery { deviceTypes: Array; } + +export interface ClaimRequest { + secretKey: string; +} + +export enum ClaimResponse { + SUCCESS = 'SUCCESS', + FAILURE = 'FAILURE', + CLAIMED = 'CLAIMED' +} + +export interface ClaimResult { + device: Device, + response: ClaimResponse +} diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts index eb3fc2d387..502f470f20 100644 --- a/ui-ngx/src/app/shared/models/entity.models.ts +++ b/ui-ngx/src/app/shared/models/entity.models.ts @@ -53,3 +53,89 @@ export interface ImportEntitiesResultInfo { entity: number; }; } + +export interface EntityField { + keyName: string; + value: string; + name: string; + time?: boolean; +} + +export const entityFields: {[fieldName: string]: EntityField} = { + createdTime: { + keyName: 'createdTime', + name: 'entity-field.created-time', + value: 'createdTime', + time: true + }, + name: { + keyName: 'name', + name: 'entity-field.name', + value: 'name' + }, + type: { + keyName: 'type', + name: 'entity-field.type', + value: 'type' + }, + firstName: { + keyName: 'firstName', + name: 'entity-field.first-name', + value: 'firstName' + }, + lastName: { + keyName: 'lastName', + name: 'entity-field.last-name', + value: 'lastName' + }, + email: { + keyName: 'email', + name: 'entity-field.email', + value: 'email' + }, + title: { + keyName: 'title', + name: 'entity-field.title', + value: 'title' + }, + country: { + keyName: 'country', + name: 'entity-field.country', + value: 'country' + }, + state: { + keyName: 'state', + name: 'entity-field.state', + value: 'state' + }, + city: { + keyName: 'city', + name: 'entity-field.city', + value: 'city' + }, + address: { + keyName: 'address', + name: 'entity-field.address', + value: 'address' + }, + address2: { + keyName: 'address2', + name: 'entity-field.address2', + value: 'address2' + }, + zip: { + keyName: 'zip', + name: 'entity-field.zip', + value: 'zip' + }, + phone: { + keyName: 'phone', + name: 'entity-field.phone', + value: 'phone' + }, + label: { + keyName: 'label', + name: 'entity-field.label', + value: 'label' + } +}; diff --git a/ui-ngx/src/app/shared/models/relation.models.ts b/ui-ngx/src/app/shared/models/relation.models.ts index 26a34f2f46..0cb4f20ca9 100644 --- a/ui-ngx/src/app/shared/models/relation.models.ts +++ b/ui-ngx/src/app/shared/models/relation.models.ts @@ -64,6 +64,7 @@ export interface RelationsSearchParameters { direction: EntitySearchDirection; relationTypeGroup?: RelationTypeGroup; maxLevel?: number; + fetchLastLevelOnly?: boolean; } export interface EntityRelationsQuery { 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 8be79c8ed1..d98a02e471 100644 --- a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts +++ b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts @@ -26,7 +26,8 @@ export enum DataKeyType { timeseries = 'timeseries', attribute = 'attribute', function = 'function', - alarm = 'alarm' + alarm = 'alarm', + entityField = 'entityField' } export enum LatestTelemetry {