Propagate UI changes

This commit is contained in:
Igor Kulikov 2020-02-21 19:04:49 +02:00
parent 416432e3ed
commit 44a938d8f6
16 changed files with 255 additions and 27 deletions

View File

@ -57,6 +57,14 @@ export class AppComponent implements OnInit {
'0,10 0 0,1 2,12A10,10 0 0,1 12,2Z" /></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'alpha-e-circle-outline',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 24 24"><path d="M9,7H15V9H11V11H15V13H11V15H15V17H9V7M12,2A10,10 0 0,'+
'1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 ' +
'0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z" /></svg>'
)
);
this.storageService.testLocalStorage();

View File

@ -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)
) {

View File

@ -45,11 +45,11 @@ export class AttributeService {
defaultHttpOptionsFromConfig(config));
}
public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>,
public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false,
config?: RequestConfig): Observable<any> {
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<any>;
if (deleteTimeseries.length) {
deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, config);
deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, config);
} else {
deleteEntityTimeseriesObservable = of(null);
}

View File

@ -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<Device>(`/api/tenant/devices?deviceName=${deviceName}`, defaultHttpOptionsFromConfig(config));
}
public claimDevice(deviceName: string, claimRequest: ClaimRequest,
config?: RequestConfig): Observable<ClaimResult> {
return this.http.post<ClaimResult>(`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));
}
}

View File

@ -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<string> {
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<Array<string>> {
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<Array<BaseData<EntityId>>>;
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 {

View File

@ -133,6 +133,14 @@
</tb-entity-select>
</div>
</div>
<div fxFlex fxLayout="row">
<section class="tb-root-state-entity-switch" fxLayout="row" fxLayoutAlign="start center" style="padding-left: 0px;">
<mat-slide-toggle class="root-state-entity-switch"
formControlName="fetchLastLevelOnly">
</mat-slide-toggle>
<label class="tb-small root-state-entity-label" translate>alias.last-level-relation</label>
</section>
</div>
<div fxFlex fxLayoutGap="8px" fxLayout="row">
<mat-form-field class="mat-block" style="min-width: 100px;">
<mat-label translate>relation.direction</mat-label>
@ -191,6 +199,14 @@
</tb-entity-select>
</div>
</div>
<div fxFlex fxLayout="row">
<section class="tb-root-state-entity-switch" fxLayout="row" fxLayoutAlign="start center" style="padding-left: 0px;">
<mat-slide-toggle class="root-state-entity-switch"
formControlName="fetchLastLevelOnly">
</mat-slide-toggle>
<label class="tb-small root-state-entity-label" translate>alias.last-level-relation</label>
</section>
</div>
<div fxFlex fxLayoutGap="8px" fxLayout="row">
<mat-form-field class="mat-block" style="min-width: 100px;">
<mat-label translate>relation.direction</mat-label>

View File

@ -25,6 +25,7 @@
.tb-root-state-entity-switch {
padding-left: 10px;
padding-bottom: 10px;
.root-state-entity-switch {
margin: 0;

View File

@ -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]);

View File

@ -38,6 +38,12 @@
<mat-icon class="tb-mat-20"
svgIcon="alpha-a-circle-outline"></mat-icon>
</span>
<span *ngIf="key.type === dataKeyTypes.entityField"
matTooltip="{{'datakey.entity-field' | translate }}"
matTooltipPosition="above">
<mat-icon class="tb-mat-20"
svgIcon="alpha-e-circle-outline"></mat-icon>
</span>
<span *ngIf="key.type === dataKeyTypes.timeseries"
matTooltip="{{'datakey.timeseries' | translate }}"
matTooltipPosition="above">
@ -87,6 +93,12 @@
<mat-icon class="tb-mat-16"
svgIcon="alpha-a-circle-outline"></mat-icon>
</span>
<span *ngIf="key.type === dataKeyTypes.entityField"
matTooltip="{{'datakey.entity-field' | translate }}"
matTooltipPosition="above">
<mat-icon class="tb-mat-16"
svgIcon="alpha-e-circle-outline"></mat-icon>
</span>
<span *ngIf="key.type === dataKeyTypes.timeseries"
matTooltip="{{'datakey.timeseries' | translate }}"
matTooltipPosition="above">
@ -114,15 +126,22 @@
<span *ngIf="widgetType == widgetTypes.latest"
matTooltip="{{'datakey.attributes' | translate }}"
matTooltipPosition="above">
<mat-icon (click)="createKey(searchText, dataKeyTypes.attribute)"
<mat-icon (click)="createKey(searchText, dataKeyTypes.attribute)"
class="tb-mat-16"
svgIcon="alpha-a-circle-outline"></mat-icon>
</span>
</span>
<span *ngIf="widgetType == widgetTypes.latest"
matTooltip="{{'datakey.entity-field' | translate }}"
matTooltipPosition="above">
<mat-icon (click)="createKey(searchText, dataKeyTypes.entityField)"
class="tb-mat-16"
svgIcon="alpha-e-circle-outline"></mat-icon>
</span>
<span matTooltip="{{'datakey.timeseries' | translate }}"
matTooltipPosition="above">
<mat-icon (click)="createKey(searchText, dataKeyTypes.timeseries)"
<mat-icon (click)="createKey(searchText, dataKeyTypes.timeseries)"
class="tb-mat-16">timeline</mat-icon>
</span>
</span>
</ng-template>
</ng-template>
</div>

View File

@ -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 {

View File

@ -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);

View File

@ -91,6 +91,7 @@ export interface RelationsQueryFilter {
direction?: EntitySearchDirection;
filters?: Array<EntityTypeFilter>;
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 {

View File

@ -57,3 +57,18 @@ export interface DeviceCredentials extends BaseData<DeviceCredentialsId> {
export interface DeviceSearchQuery extends EntitySearchQuery {
deviceTypes: Array<string>;
}
export interface ClaimRequest {
secretKey: string;
}
export enum ClaimResponse {
SUCCESS = 'SUCCESS',
FAILURE = 'FAILURE',
CLAIMED = 'CLAIMED'
}
export interface ClaimResult {
device: Device,
response: ClaimResponse
}

View File

@ -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'
}
};

View File

@ -64,6 +64,7 @@ export interface RelationsSearchParameters {
direction: EntitySearchDirection;
relationTypeGroup?: RelationTypeGroup;
maxLevel?: number;
fetchLastLevelOnly?: boolean;
}
export interface EntityRelationsQuery {

View File

@ -26,7 +26,8 @@ export enum DataKeyType {
timeseries = 'timeseries',
attribute = 'attribute',
function = 'function',
alarm = 'alarm'
alarm = 'alarm',
entityField = 'entityField'
}
export enum LatestTelemetry {