UI: Refactor entities hierarchy widget to use new WS data query API.

This commit is contained in:
Igor Kulikov 2020-07-06 18:00:10 +03:00
parent 2eb7949d57
commit 92c4d00fbb
5 changed files with 141 additions and 145 deletions

File diff suppressed because one or more lines are too long

View File

@ -8997,10 +8997,10 @@
"integrity": "sha512-4O3GWAYJaauMCILm07weko2rHA8a4kjn7+8Lg4s1d7SxwS/3IpkVD/GljbRrIJ1c1W/XGJ3GbuK7RyYZEJChhw==" "integrity": "sha512-4O3GWAYJaauMCILm07weko2rHA8a4kjn7+8Lg4s1d7SxwS/3IpkVD/GljbRrIJ1c1W/XGJ3GbuK7RyYZEJChhw=="
}, },
"ngx-flowchart": { "ngx-flowchart": {
"version": "git://github.com/thingsboard/ngx-flowchart.git#a4157b0eef2eb3646ef920447c7b06b39d54f87f", "version": "git://github.com/thingsboard/ngx-flowchart.git#7a02f4748b5e7821a883c903107af5f20415d026",
"from": "git://github.com/thingsboard/ngx-flowchart.git#master", "from": "git://github.com/thingsboard/ngx-flowchart.git#master",
"requires": { "requires": {
"tslib": "^1.10.0" "tslib": "^1.13.0"
}, },
"dependencies": { "dependencies": {
"tslib": { "tslib": {

View File

@ -20,7 +20,7 @@ import { Datasource, DatasourceType } 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';
import { EntityAliases } from '@shared/models/alias.models'; import { AliasFilterType, EntityAliases, SingleEntityFilter } from '@shared/models/alias.models';
import { EntityInfo } from '@shared/models/entity.models'; import { EntityInfo } from '@shared/models/entity.models';
import { map, mergeMap } from 'rxjs/operators'; import { map, mergeMap } from 'rxjs/operators';
import { import {
@ -282,6 +282,15 @@ export class AliasController implements IAliasController {
} }
}) })
); );
} else if (newDatasource.entityId && !newDatasource.entityFilter) {
newDatasource.entityFilter = {
singleEntity: {
id: newDatasource.entityId,
entityType: newDatasource.entityType,
},
type: AliasFilterType.singleEntity
} as SingleEntityFilter;
return of(newDatasource);
} else { } else {
newDatasource.aliasName = newDatasource.entityName; newDatasource.aliasName = newDatasource.entityName;
newDatasource.name = newDatasource.entityName; newDatasource.name = newDatasource.entityName;

View File

@ -23,19 +23,18 @@ import { DatasourceData, DatasourceType, WidgetConfig, widgetType } from '@share
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { forkJoin, fromEvent, Observable, of } from 'rxjs'; import { fromEvent } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, mergeMap, tap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
import { constructTableCssString } from '@home/components/widget/lib/table-widget.models'; import { constructTableCssString } from '@home/components/widget/lib/table-widget.models';
import { Overlay } from '@angular/cdk/overlay'; import { Overlay } from '@angular/cdk/overlay';
import { import {
LoadNodesCallback, LoadNodesCallback,
NavTreeEditCallbacks, NavTreeEditCallbacks,
NodesCallback,
NodeSearchCallback, NodeSearchCallback,
NodeSelectedCallback, NodeSelectedCallback,
NodesInsertedCallback NodesInsertedCallback
} from '@shared/components/nav-tree.component'; } from '@shared/components/nav-tree.component';
import { BaseData } from '@shared/models/base-data';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { deepClone, hashCode } from '@core/utils'; import { deepClone, hashCode } from '@core/utils';
import { import {
@ -58,10 +57,9 @@ import {
NodesSortFunction, NodesSortFunction,
NodeTextFunction NodeTextFunction
} from '@home/components/widget/lib/entities-hierarchy-widget.models'; } from '@home/components/widget/lib/entities-hierarchy-widget.models';
import { EntityService } from '@core/http/entity.service'; import { EntityRelationsQuery } from '@shared/models/relation.models';
import { EntityRelationsQuery, EntitySearchDirection } from '@shared/models/relation.models'; import { AliasFilterType, RelationsQueryFilter } from '@shared/models/alias.models';
import { EntityRelationService } from '@core/http/entity-relation.service'; import { EntityFilter } from '@shared/models/query/query.models';
import { ActionNotificationShow } from '@core/notification/notification.actions';
@Component({ @Component({
selector: 'tb-entities-hierarchy-widget', selector: 'tb-entities-hierarchy-widget',
@ -86,6 +84,7 @@ 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} = {};
@ -108,14 +107,11 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
} }
}; };
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private elementRef: ElementRef, private elementRef: ElementRef,
private overlay: Overlay, private overlay: Overlay,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private utils: UtilsService, private utils: UtilsService) {
private entityService: EntityService,
private entityRelationService: EntityRelationService) {
super(store); super(store);
} }
@ -125,6 +121,7 @@ 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();
} }
@ -254,33 +251,15 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
public loadNodes: LoadNodesCallback = (node, cb) => { public loadNodes: LoadNodesCallback = (node, cb) => {
if (node.id === '#') { if (node.id === '#') {
const tasks: Observable<HierarchyNavTreeNode>[] = []; const childNodes: HierarchyNavTreeNode[] = [];
this.datasources.forEach((datasource) => { this.datasources.forEach((childDatasource, index) => {
tasks.push(this.datasourceToNode(datasource)); childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, this.data[index]));
});
forkJoin(tasks).subscribe((nodes) => {
cb(this.prepareNodes(nodes));
this.updateNodeData(this.subscription.data);
}); });
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.nodeCtx.entity.id.entityType !== 'function') {
const relationQuery = this.prepareNodeRelationQuery(node.data.nodeCtx); this.loadChildren(node, node.data.datasource, cb);
this.entityRelationService.findByQuery(relationQuery, {ignoreErrors: true, ignoreLoading: true}).subscribe( /* (error) => { // TODO:
(entityRelations) => {
if (entityRelations.length) {
const tasks: Observable<HierarchyNavTreeNode>[] = [];
entityRelations.forEach((relation) => {
const targetId = relationQuery.parameters.direction === EntitySearchDirection.FROM ? relation.to : relation.from;
tasks.push(this.entityIdToNode(targetId.entityType as EntityType, targetId.id, node.data.datasource, node.data.nodeCtx));
});
forkJoin(tasks).subscribe((nodes) => {
cb(this.prepareNodes(nodes));
});
} else {
cb([]);
}
},
(error) => {
let errorText = 'Failed to get relations!'; let errorText = 'Failed to get relations!';
if (error && error.status === 400) { if (error && error.status === 400) {
errorText = 'Invalid relations query returned by \'Node relations query function\'! Please check widget configuration!'; errorText = 'Invalid relations query returned by \'Node relations query function\'! Please check widget configuration!';
@ -288,6 +267,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
this.showError(errorText); this.showError(errorText);
} }
); );
*/
} else { } else {
cb([]); cb([]);
} }
@ -313,7 +293,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
} }
} }
public onNodesInserted: NodesInsertedCallback = (nodes, parent) => { public onNodesInserted: NodesInsertedCallback = (nodes) => {
if (nodes) { if (nodes) {
nodes.forEach((nodeId) => { nodes.forEach((nodeId) => {
const task = this.pendingUpdateNodeTasks[nodeId]; const task = this.pendingUpdateNodeTasks[nodeId];
@ -355,17 +335,6 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
} }
} }
private showError(errorText: string) {
this.store.dispatch(new ActionNotificationShow(
{
message: errorText,
type: 'error',
target: this.toastTargetId,
verticalPosition: 'bottom',
horizontalPosition: 'left'
}));
}
private prepareNodes(nodes: HierarchyNavTreeNode[]): HierarchyNavTreeNode[] { private prepareNodes(nodes: HierarchyNavTreeNode[]): HierarchyNavTreeNode[] {
nodes = nodes.filter((node) => node !== null); nodes = nodes.filter((node) => node !== null);
nodes.sort((node1, node2) => this.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx)); nodes.sort((node1, node2) => this.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx));
@ -399,85 +368,87 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
} }
} }
private datasourceToNode(datasource: HierarchyNodeDatasource, parentNodeCtx?: HierarchyNodeContext): Observable<HierarchyNavTreeNode> { private datasourceToNode(datasource: HierarchyNodeDatasource,
return this.resolveEntity(datasource).pipe( data: DatasourceData[],
map(entity => { parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode {
if (entity !== null) { const node: HierarchyNavTreeNode = {
const node: HierarchyNavTreeNode = { id: (++this.nodeIdCounter) + ''
id: (++this.nodeIdCounter) + '' };
}; this.nodesMap[node.id] = node;
this.nodesMap[node.id] = node; datasource.nodeId = node.id;
datasource.nodeId = node.id; node.icon = false;
node.icon = false; const nodeCtx: HierarchyNodeContext = {
const nodeCtx: HierarchyNodeContext = { parentNodeCtx,
parentNodeCtx, entity: {
entity,
data: {}
};
nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1;
node.data = {
datasource,
nodeCtx
};
node.state = {
disabled: this.nodeDisabledFunction(node.data.nodeCtx),
opened: this.nodeOpenedFunction(node.data.nodeCtx)
};
node.text = this.prepareNodeText(node);
node.children = this.nodeHasChildrenFunction(node.data.nodeCtx);
return node;
} else {
return null;
}
})
);
}
private entityIdToNode(entityType: EntityType, entityId: string,
parentDatasource: HierarchyNodeDatasource,
parentNodeCtx: HierarchyNodeContext): Observable<HierarchyNavTreeNode> {
const datasource = {
dataKeys: parentDatasource.dataKeys,
type: DatasourceType.entity,
entityType,
entityId
} as HierarchyNodeDatasource;
return this.datasourceToNode(datasource, parentNodeCtx).pipe(
mergeMap((node) => {
if (node != null) {
const subscriptionOptions: WidgetSubscriptionOptions = {
type: widgetType.latest,
datasources: [datasource],
callbacks: {
onDataUpdated: subscription => {
this.updateNodeData(subscription.data);
}
}
};
return this.ctx.subscriptionApi.
createSubscription(subscriptionOptions, true).pipe(
map(() => node));
} else {
return of(node);
}
})
);
}
private resolveEntity(datasource: HierarchyNodeDatasource): Observable<BaseData<EntityId>> {
if (datasource.type === DatasourceType.function) {
const entity = {
id: { id: {
entityType: 'function' id: datasource.entityId,
entityType: datasource.entityType
}, },
name: datasource.name name: datasource.entityName,
}; label: datasource.entityLabel ? datasource.entityLabel : datasource.entityName
return of(entity as BaseData<EntityId>); },
} else { data: {}
return this.entityService.getEntity(datasource.entityType, datasource.entityId, {ignoreLoading: true}).pipe( };
catchError(err => of(null)) datasource.dataKeys.forEach((dataKey, index) => {
); const keyData = data[index].data;
} if (keyData && keyData.length && keyData[0].length > 1) {
nodeCtx.data[dataKey.label] = keyData[0][1];
} else {
nodeCtx.data[dataKey.label] = '';
}
});
nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1;
node.data = {
datasource,
nodeCtx
};
node.state = {
disabled: this.nodeDisabledFunction(node.data.nodeCtx),
opened: this.nodeOpenedFunction(node.data.nodeCtx)
};
node.text = this.prepareNodeText(node);
node.children = this.nodeHasChildrenFunction(node.data.nodeCtx);
return node;
}
private loadChildren(parentNode: HierarchyNavTreeNode, datasource: HierarchyNodeDatasource, childrenNodesLoadCb: NodesCallback) {
const nodeCtx = parentNode.data.nodeCtx;
nodeCtx.childrenNodesLoaded = false;
const entityFilter = this.prepareNodeRelationsQueryFilter(nodeCtx);
const childrenDatasource = {
dataKeys: datasource.dataKeys,
type: DatasourceType.entity,
entityFilter
} as HierarchyNodeDatasource;
const subscriptionOptions: WidgetSubscriptionOptions = {
type: widgetType.latest,
datasources: [childrenDatasource],
callbacks: {
onSubscriptionMessage: (subscription, message) => {
this.ctx.showToast(message.severity, message.message, undefined,
'bottom', 'left', this.toastTargetId);
},
onInitialPageDataChanged: (subscription) => {
this.ctx.subscriptionApi.removeSubscription(subscription.id);
this.nodeEditCallbacks.refreshNode(parentNode.id);
},
onDataUpdated: subscription => {
if (nodeCtx.childrenNodesLoaded) {
this.updateNodeData(subscription.data);
} else {
const datasourcesPageData = subscription.datasourcePages[0];
const dataPageData = subscription.dataPages[0];
const childNodes: HierarchyNavTreeNode[] = [];
datasourcesPageData.data.forEach((childDatasource, index) => {
childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, dataPageData.data[index]));
});
nodeCtx.childrenNodesLoaded = true;
childrenNodesLoadCb(this.prepareNodes(childNodes));
}
}
}
};
this.ctx.subscriptionApi.createSubscription(subscriptionOptions, true);
} }
private prepareNodeRelationQuery(nodeCtx: HierarchyNodeContext): EntityRelationsQuery { private prepareNodeRelationQuery(nodeCtx: HierarchyNodeContext): EntityRelationsQuery {
@ -487,4 +458,19 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
} }
return relationQuery as EntityRelationsQuery; return relationQuery as EntityRelationsQuery;
} }
private prepareNodeRelationsQueryFilter(nodeCtx: HierarchyNodeContext): EntityFilter {
const relationQuery = this.prepareNodeRelationQuery(nodeCtx);
return {
rootEntity: {
id: relationQuery.parameters.rootId,
entityType: relationQuery.parameters.rootType
},
direction: relationQuery.parameters.direction,
filters: relationQuery.filters,
maxLevel: relationQuery.parameters.maxLevel,
fetchLastLevelOnly: relationQuery.parameters.fetchLastLevelOnly,
type: AliasFilterType.relationsQuery
} as RelationsQueryFilter;
}
} }

View File

@ -16,7 +16,7 @@
import { BaseData } from '@shared/models/base-data'; import { BaseData } from '@shared/models/base-data';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { NavTreeNode } from '@shared/components/nav-tree.component'; import { NavTreeNode, NodesCallback } from '@shared/components/nav-tree.component';
import { Datasource } from '@shared/models/widget.models'; import { Datasource } from '@shared/models/widget.models';
import { isDefined, isUndefined } from '@core/utils'; import { isDefined, isUndefined } from '@core/utils';
import { EntityRelationsQuery, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models'; import { EntityRelationsQuery, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models';
@ -35,6 +35,7 @@ export interface EntitiesHierarchyWidgetSettings {
export interface HierarchyNodeContext { export interface HierarchyNodeContext {
parentNodeCtx?: HierarchyNodeContext; parentNodeCtx?: HierarchyNodeContext;
entity: BaseData<EntityId>; entity: BaseData<EntityId>;
childrenNodesLoaded?: boolean;
level?: number; level?: number;
data: {[key: string]: any}; data: {[key: string]: any};
} }