9 changed files with 9 additions and 889 deletions
File diff suppressed because one or more lines are too long
@ -1,51 +0,0 @@ |
|||||
<!-- |
|
||||
|
|
||||
Copyright © 2016-2020 The Thingsboard Authors |
|
||||
|
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||
you may not use this file except in compliance with the License. |
|
||||
You may obtain a copy of the License at |
|
||||
|
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
|
||||
|
|
||||
Unless required by applicable law or agreed to in writing, software |
|
||||
distributed under the License is distributed on an "AS IS" BASIS, |
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||
See the License for the specific language governing permissions and |
|
||||
limitations under the License. |
|
||||
|
|
||||
--> |
|
||||
<div class="tb-edge-instances-overview tb-absolute-fill" tb-toast toastTarget="{{ toastTargetId }}"> |
|
||||
<div fxFlex fxLayout="column" class="tb-absolute-fill"> |
|
||||
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode"> |
|
||||
<div class="mat-toolbar-tools"> |
|
||||
<button mat-button mat-icon-button |
|
||||
matTooltip="{{ 'action.search' | translate }}" |
|
||||
matTooltipPosition="above"> |
|
||||
<mat-icon>search</mat-icon> |
|
||||
</button> |
|
||||
<mat-form-field fxFlex> |
|
||||
<mat-label> </mat-label> |
|
||||
<input #searchInput matInput |
|
||||
[(ngModel)]="textSearch" |
|
||||
placeholder="{{ 'entity.search' | translate }}"/> |
|
||||
</mat-form-field> |
|
||||
<button mat-button mat-icon-button (click)="exitFilterMode()" |
|
||||
matTooltip="{{ 'action.close' | translate }}" |
|
||||
matTooltipPosition="above"> |
|
||||
<mat-icon>close</mat-icon> |
|
||||
</button> |
|
||||
</div> |
|
||||
</mat-toolbar> |
|
||||
<div fxFlex class="tb-edges-nav-tree-panel"> |
|
||||
<tb-nav-tree |
|
||||
[loadNodes]="loadNodes" |
|
||||
[onNodeSelected]="onNodeSelected" |
|
||||
[onNodesInserted]="onNodesInserted" |
|
||||
[editCallbacks]="nodeEditCallbacks" |
|
||||
enableSearch="true" |
|
||||
[searchCallback]="searchCallback" |
|
||||
></tb-nav-tree> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
@ -1,554 +0,0 @@ |
|||||
///
|
|
||||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|
||||
///
|
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||
/// you may not use this file except in compliance with the License.
|
|
||||
/// You may obtain a copy of the License at
|
|
||||
///
|
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||
///
|
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||
/// See the License for the specific language governing permissions and
|
|
||||
/// limitations under the License.
|
|
||||
///
|
|
||||
|
|
||||
import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; |
|
||||
import { PageComponent } from '@shared/components/page.component'; |
|
||||
import { Store } from '@ngrx/store'; |
|
||||
import { AppState } from '@core/core.state'; |
|
||||
import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; |
|
||||
import { DatasourceData, DatasourceType, WidgetConfig, widgetType } from '@shared/models/widget.models'; |
|
||||
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; |
|
||||
import { UtilsService } from '@core/services/utils.service'; |
|
||||
import cssjs from '@core/css/css'; |
|
||||
import { fromEvent } from 'rxjs'; |
|
||||
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; |
|
||||
import { constructTableCssString } from '@home/components/widget/lib/table-widget.models'; |
|
||||
import { Overlay } from '@angular/cdk/overlay'; |
|
||||
import { |
|
||||
LoadNodesCallback, |
|
||||
NavTreeEditCallbacks, NodesCallback, |
|
||||
NodeSearchCallback, |
|
||||
NodeSelectedCallback, |
|
||||
NodesInsertedCallback |
|
||||
} from '@shared/components/nav-tree.component'; |
|
||||
import { EntityType } from '@shared/models/entity-type.models'; |
|
||||
import { deepClone, hashCode } from '@core/utils'; |
|
||||
import { |
|
||||
defaultNodeIconFunction, |
|
||||
defaultNodeOpenedFunction, |
|
||||
defaultNodeRelationQueryFunction, |
|
||||
defaultNodesSortFunction, |
|
||||
EdgeGroupsNodeData, |
|
||||
edgeGroupsNodeText, |
|
||||
edgeGroupsTypes, |
|
||||
EdgeNodeData, |
|
||||
edgeNodeText, |
|
||||
EdgeInstancesOverviewNode, |
|
||||
EdgeInstancesOverviewWidgetSettings, |
|
||||
HierarchyNavTreeNode, |
|
||||
HierarchyNodeContext, |
|
||||
HierarchyNodeDatasource, |
|
||||
iconUrlHtml, |
|
||||
loadNodeCtxFunction, |
|
||||
materialIconHtml, |
|
||||
NodeDisabledFunction, |
|
||||
NodeHasChildrenFunction, |
|
||||
NodeIconFunction, |
|
||||
NodeOpenedFunction, |
|
||||
NodeRelationQueryFunction, |
|
||||
NodesSortFunction, |
|
||||
NodeTextFunction |
|
||||
} from '@home/components/widget/lib/edge-instances-overview-widget.models'; |
|
||||
import { EdgeService } from "@core/http/edge.service"; |
|
||||
import { PageLink } from "@shared/models/page/page-link"; |
|
||||
import { Edge, EdgeInfo } from "@shared/models/edge.models"; |
|
||||
import { TranslateService } from "@ngx-translate/core"; |
|
||||
import { EntityService } from "@core/http/entity.service"; |
|
||||
import { Direction, SortOrder } from "@shared/models/page/sort-order"; |
|
||||
import { EntityRelationsQuery } from "@shared/models/relation.models"; |
|
||||
import { EntityFilter } from "@shared/models/query/query.models"; |
|
||||
import { AliasFilterType, RelationsQueryFilter } from "@shared/models/alias.models"; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'tb-edge-instances-overview-widget', |
|
||||
templateUrl: './edge-instances-overview-widget.component.html', |
|
||||
styleUrls: ['./edge-instances-overview-widget.component.scss'] |
|
||||
}) |
|
||||
export class EdgeInstancesOverviewWidgetComponent extends PageComponent implements OnInit, AfterViewInit { |
|
||||
|
|
||||
@Input() |
|
||||
ctx: WidgetContext; |
|
||||
|
|
||||
@ViewChild('searchInput') searchInputField: ElementRef; |
|
||||
|
|
||||
public toastTargetId = 'edge-instances-overview-' + this.utils.guid(); |
|
||||
|
|
||||
public textSearchMode = false; |
|
||||
public textSearch = null; |
|
||||
|
|
||||
public nodeEditCallbacks: NavTreeEditCallbacks = {}; |
|
||||
|
|
||||
private settings: EdgeInstancesOverviewWidgetSettings; |
|
||||
private widgetConfig: WidgetConfig; |
|
||||
private subscription: IWidgetSubscription; |
|
||||
private datasources: Array<HierarchyNodeDatasource>; |
|
||||
private data: Array<Array<DatasourceData>>; |
|
||||
|
|
||||
private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {}; |
|
||||
private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {}; |
|
||||
private nodeIdCounter = 0; |
|
||||
|
|
||||
private nodeRelationQueryFunction: NodeRelationQueryFunction; |
|
||||
private nodeIconFunction: NodeIconFunction; |
|
||||
private nodeTextFunction: NodeTextFunction; |
|
||||
private nodeDisabledFunction: NodeDisabledFunction; |
|
||||
private nodeOpenedFunction: NodeOpenedFunction; |
|
||||
private nodeHasChildrenFunction: NodeHasChildrenFunction; |
|
||||
private nodesSortFunction: NodesSortFunction; |
|
||||
|
|
||||
private edgeNodesMap: {[parentNodeId: string]: {[edgeId: string]: string}} = {}; |
|
||||
private edgeGroupsNodesMap: {[edgeNodeId: string]: {[groupType: string]: string}} = {}; |
|
||||
|
|
||||
|
|
||||
private searchAction: WidgetAction = { |
|
||||
name: 'action.search', |
|
||||
show: false, |
|
||||
icon: 'search', |
|
||||
onAction: () => { |
|
||||
this.enterFilterMode(); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
constructor(protected store: Store<AppState>, |
|
||||
private elementRef: ElementRef, |
|
||||
private edgeService: EdgeService, |
|
||||
private entityService: EntityService, |
|
||||
private translateService: TranslateService, |
|
||||
private overlay: Overlay, |
|
||||
private viewContainerRef: ViewContainerRef, |
|
||||
private utils: UtilsService) { |
|
||||
super(store); |
|
||||
} |
|
||||
|
|
||||
ngOnInit(): void { |
|
||||
this.ctx.$scope.edgeInstancesOverviewWidget = this; |
|
||||
this.settings = this.ctx.settings; |
|
||||
this.widgetConfig = this.ctx.widgetConfig; |
|
||||
this.subscription = this.ctx.defaultSubscription; |
|
||||
this.datasources = this.subscription.datasources as Array<HierarchyNodeDatasource>; |
|
||||
this.data = this.subscription.dataPages[0].data; |
|
||||
this.initializeConfig(); |
|
||||
this.ctx.updateWidgetParams(); |
|
||||
} |
|
||||
|
|
||||
ngAfterViewInit(): void { |
|
||||
fromEvent(this.searchInputField.nativeElement, 'keyup') |
|
||||
.pipe( |
|
||||
debounceTime(150), |
|
||||
distinctUntilChanged(), |
|
||||
tap(() => { |
|
||||
this.updateSearchNodes(); |
|
||||
}) |
|
||||
) |
|
||||
.subscribe(); |
|
||||
} |
|
||||
|
|
||||
public onDataUpdated() { |
|
||||
this.updateNodeData(this.subscription.data); |
|
||||
} |
|
||||
|
|
||||
private initializeConfig() { |
|
||||
this.ctx.widgetActions = [this.searchAction]; |
|
||||
|
|
||||
const testNodeCtx: HierarchyNodeContext = { |
|
||||
entity: { |
|
||||
id: { |
|
||||
entityType: EntityType.DEVICE, |
|
||||
id: '123' |
|
||||
}, |
|
||||
name: 'TEST DEV1' |
|
||||
}, |
|
||||
data: {}, |
|
||||
level: 2 |
|
||||
}; |
|
||||
const parentNodeCtx = deepClone(testNodeCtx); |
|
||||
parentNodeCtx.level = 1; |
|
||||
testNodeCtx.parentNodeCtx = parentNodeCtx; |
|
||||
|
|
||||
this.nodeRelationQueryFunction = loadNodeCtxFunction(this.settings.nodeRelationQueryFunction, 'nodeCtx', testNodeCtx); |
|
||||
this.nodeIconFunction = loadNodeCtxFunction(this.settings.nodeIconFunction, 'nodeCtx', testNodeCtx); |
|
||||
this.nodeTextFunction = loadNodeCtxFunction(this.settings.nodeTextFunction, 'nodeCtx', testNodeCtx); |
|
||||
this.nodeDisabledFunction = loadNodeCtxFunction(this.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx); |
|
||||
this.nodeOpenedFunction = loadNodeCtxFunction(this.settings.nodeOpenedFunction, 'nodeCtx', testNodeCtx); |
|
||||
this.nodeHasChildrenFunction = loadNodeCtxFunction(this.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx); |
|
||||
|
|
||||
const testNodeCtx2 = deepClone(testNodeCtx); |
|
||||
testNodeCtx2.entity.name = 'TEST DEV2'; |
|
||||
|
|
||||
this.nodesSortFunction = loadNodeCtxFunction(this.settings.nodesSortFunction, 'nodeCtx1,nodeCtx2', testNodeCtx, testNodeCtx2); |
|
||||
|
|
||||
this.nodeRelationQueryFunction = this.nodeRelationQueryFunction || defaultNodeRelationQueryFunction; |
|
||||
this.nodeIconFunction = this.nodeIconFunction || defaultNodeIconFunction; |
|
||||
this.nodeTextFunction = this.nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name); |
|
||||
this.nodeDisabledFunction = this.nodeDisabledFunction || (() => false); |
|
||||
this.nodeOpenedFunction = this.nodeOpenedFunction || defaultNodeOpenedFunction; |
|
||||
this.nodeHasChildrenFunction = this.nodeHasChildrenFunction || (() => true); |
|
||||
this.nodesSortFunction = this.nodesSortFunction || defaultNodesSortFunction; |
|
||||
|
|
||||
const cssString = constructTableCssString(this.widgetConfig); |
|
||||
const cssParser = new cssjs(); |
|
||||
cssParser.testMode = false; |
|
||||
const namespace = 'edges-instances-overview-' + hashCode(cssString); |
|
||||
cssParser.cssPreviewNamespace = namespace; |
|
||||
cssParser.createStyleElement(namespace, cssString); |
|
||||
$(this.elementRef.nativeElement).addClass(namespace); |
|
||||
} |
|
||||
|
|
||||
private enterFilterMode() { |
|
||||
this.textSearchMode = true; |
|
||||
this.textSearch = ''; |
|
||||
this.ctx.hideTitlePanel = true; |
|
||||
this.ctx.detectChanges(true); |
|
||||
setTimeout(() => { |
|
||||
this.searchInputField.nativeElement.focus(); |
|
||||
this.searchInputField.nativeElement.setSelectionRange(0, 0); |
|
||||
}, 10); |
|
||||
} |
|
||||
|
|
||||
exitFilterMode() { |
|
||||
this.textSearchMode = false; |
|
||||
this.textSearch = null; |
|
||||
this.updateSearchNodes(); |
|
||||
this.ctx.hideTitlePanel = false; |
|
||||
this.ctx.detectChanges(true); |
|
||||
} |
|
||||
|
|
||||
private updateSearchNodes() { |
|
||||
if (this.textSearch != null) { |
|
||||
this.nodeEditCallbacks.search(this.textSearch); |
|
||||
} else { |
|
||||
this.nodeEditCallbacks.clearSearch(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private updateNodeData(subscriptionData: Array<DatasourceData>) { |
|
||||
const affectedNodes: string[] = []; |
|
||||
if (subscriptionData) { |
|
||||
subscriptionData.forEach((datasourceData) => { |
|
||||
const datasource = datasourceData.datasource as HierarchyNodeDatasource; |
|
||||
if (datasource.nodeId) { |
|
||||
const node = this.nodesMap[datasource.nodeId]; |
|
||||
const key = datasourceData.dataKey.label; |
|
||||
let value; |
|
||||
if (datasourceData.data && datasourceData.data.length) { |
|
||||
value = datasourceData.data[0][1]; |
|
||||
} |
|
||||
if (node.data.nodeCtx.data[key] !== value) { |
|
||||
if (affectedNodes.indexOf(datasource.nodeId) === -1) { |
|
||||
affectedNodes.push(datasource.nodeId); |
|
||||
} |
|
||||
node.data.nodeCtx.data[key] = value; |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
affectedNodes.forEach((nodeId) => { |
|
||||
const node: HierarchyNavTreeNode = this.nodeEditCallbacks.getNode(nodeId); |
|
||||
if (node) { |
|
||||
this.updateNodeStyle(this.nodesMap[nodeId]); |
|
||||
} else { |
|
||||
this.pendingUpdateNodeTasks[nodeId] = () => { |
|
||||
this.updateNodeStyle(this.nodesMap[nodeId]); |
|
||||
}; |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public loadNodes: LoadNodesCallback = (node, cb) => { |
|
||||
if (node.id === '#') { |
|
||||
const sortOrder: SortOrder = { property: 'name', direction: Direction.ASC }; |
|
||||
const pageLink = new PageLink(100, 0, null, sortOrder); |
|
||||
this.edgeService.getTenantEdgeInfos(pageLink).subscribe( |
|
||||
(edges) => { |
|
||||
cb(this.edgesToNodes(node.id, edges.data)) |
|
||||
}); |
|
||||
} else if (node.data.type === 'edge') { |
|
||||
const edge = node.data.entity; |
|
||||
cb(this.loadNodesForEdge(node.id, edge)); |
|
||||
} else if (node.data.type === 'edgeGroups') { |
|
||||
const pageLink = new PageLink(100); |
|
||||
this.entityService.getAssignedToEdgeEntitiesByType(node, pageLink).subscribe( |
|
||||
(entities) => { |
|
||||
if (entities.data.length > 0) { |
|
||||
cb(this.edgesToNodes(node.id, entities.data)); |
|
||||
} else { |
|
||||
cb([]); |
|
||||
} |
|
||||
} |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private loadNodesForEdge(parentNodeId: string, edge: EdgeInfo): EdgeInstancesOverviewNode[] { |
|
||||
const nodes: EdgeInstancesOverviewNode[] = []; |
|
||||
const nodesMap = {}; |
|
||||
this.edgeGroupsNodesMap[parentNodeId] = nodesMap; |
|
||||
edgeGroupsTypes.forEach((entityType) => { |
|
||||
const node: EdgeInstancesOverviewNode = { |
|
||||
id: (++this.nodeIdCounter)+'', |
|
||||
icon: false, |
|
||||
text: edgeGroupsNodeText(this.translateService, entityType), |
|
||||
children: true, |
|
||||
data: { |
|
||||
type: 'edgeGroups', |
|
||||
entityType, |
|
||||
edge, |
|
||||
internalId: edge.id.id + '_' + entityType |
|
||||
} as EdgeGroupsNodeData |
|
||||
}; |
|
||||
nodes.push(node); |
|
||||
nodesMap[entityType] = node.id; |
|
||||
}); |
|
||||
return nodes; |
|
||||
} |
|
||||
|
|
||||
private createEdgeNode(parentNodeId: string, edge: Edge): EdgeInstancesOverviewNode { |
|
||||
let nodesMap = this.edgeNodesMap[parentNodeId]; |
|
||||
if (!nodesMap) { |
|
||||
nodesMap = {}; |
|
||||
this.edgeNodesMap[parentNodeId] = nodesMap; |
|
||||
} |
|
||||
const node: EdgeInstancesOverviewNode = { |
|
||||
id: (++this.nodeIdCounter)+'', |
|
||||
icon: false, |
|
||||
text: edgeNodeText(edge), |
|
||||
children: parentNodeId === '#', |
|
||||
state: { |
|
||||
disabled: false |
|
||||
}, |
|
||||
data: { |
|
||||
type: 'edge', |
|
||||
entity: edge, |
|
||||
internalId: edge.id.id |
|
||||
} as EdgeNodeData |
|
||||
}; |
|
||||
nodesMap[edge.id.id] = node.id; |
|
||||
return node; |
|
||||
} |
|
||||
|
|
||||
private edgesToNodes(parentNodeId: string, edges: Array<Edge>): EdgeInstancesOverviewNode[] { |
|
||||
const nodes: EdgeInstancesOverviewNode[] = []; |
|
||||
this.edgeNodesMap[parentNodeId] = {}; |
|
||||
if (edges) { |
|
||||
edges.forEach((edge) => { |
|
||||
const node = this.createEdgeNode(parentNodeId, edge); |
|
||||
nodes.push(node); |
|
||||
}); |
|
||||
} |
|
||||
return nodes; |
|
||||
} |
|
||||
|
|
||||
public onNodeSelected: NodeSelectedCallback = (node, event) => { |
|
||||
let nodeId; |
|
||||
if (!node) { |
|
||||
nodeId = -1; |
|
||||
} else { |
|
||||
nodeId = node.id; |
|
||||
} |
|
||||
if (nodeId !== -1) { |
|
||||
const selectedNode = this.nodesMap[nodeId]; |
|
||||
if (selectedNode) { |
|
||||
const descriptors = this.ctx.actionsApi.getActionDescriptors('nodeSelected'); |
|
||||
if (descriptors.length) { |
|
||||
const entity = selectedNode.data.nodeCtx.entity; |
|
||||
this.ctx.actionsApi.handleWidgetAction(event, descriptors[0], entity.id, entity.name, { nodeCtx: selectedNode.data.nodeCtx }); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public onNodesInserted: NodesInsertedCallback = (nodes) => { |
|
||||
if (nodes) { |
|
||||
nodes.forEach((nodeId) => { |
|
||||
const task = this.pendingUpdateNodeTasks[nodeId]; |
|
||||
if (task) { |
|
||||
task(); |
|
||||
delete this.pendingUpdateNodeTasks[nodeId]; |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public searchCallback: NodeSearchCallback = (searchText, node) => { |
|
||||
const theNode = this.nodesMap[node.id]; |
|
||||
if (theNode && theNode.data.searchText) { |
|
||||
return theNode.data.searchText.includes(searchText.toLowerCase()); |
|
||||
} |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
private updateNodeStyle(node: HierarchyNavTreeNode) { |
|
||||
const newText = this.prepareNodeText(node); |
|
||||
if (node.text !== newText) { |
|
||||
node.text = newText; |
|
||||
this.nodeEditCallbacks.updateNode(node.id, node.text); |
|
||||
} |
|
||||
const newDisabled = this.nodeDisabledFunction(node.data.nodeCtx); |
|
||||
if (node.state.disabled !== newDisabled) { |
|
||||
node.state.disabled = newDisabled; |
|
||||
if (node.state.disabled) { |
|
||||
this.nodeEditCallbacks.disableNode(node.id); |
|
||||
} else { |
|
||||
this.nodeEditCallbacks.enableNode(node.id); |
|
||||
} |
|
||||
} |
|
||||
const newHasChildren = this.nodeHasChildrenFunction(node.data.nodeCtx); |
|
||||
if (node.children !== newHasChildren) { |
|
||||
node.children = newHasChildren; |
|
||||
this.nodeEditCallbacks.setNodeHasChildren(node.id, node.children); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private prepareNodes(nodes: HierarchyNavTreeNode[]): HierarchyNavTreeNode[] { |
|
||||
nodes = nodes.filter((node) => node !== null); |
|
||||
nodes.sort((node1, node2) => this.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx)); |
|
||||
return nodes; |
|
||||
} |
|
||||
|
|
||||
private prepareNodeText(node: HierarchyNavTreeNode): string { |
|
||||
const nodeIcon = this.prepareNodeIcon(node.data.nodeCtx); |
|
||||
const nodeText = this.nodeTextFunction(node.data.nodeCtx); |
|
||||
node.data.searchText = nodeText ? nodeText.replace(/<[^>]+>/g, '').toLowerCase() : ''; |
|
||||
return nodeIcon + nodeText; |
|
||||
} |
|
||||
|
|
||||
private prepareNodeIcon(nodeCtx: HierarchyNodeContext): string { |
|
||||
let iconInfo = this.nodeIconFunction(nodeCtx); |
|
||||
if (iconInfo) { |
|
||||
if (iconInfo === 'default') { |
|
||||
iconInfo = defaultNodeIconFunction(nodeCtx); |
|
||||
} |
|
||||
if (iconInfo && iconInfo !== 'default' && (iconInfo.iconUrl || iconInfo.materialIcon)) { |
|
||||
if (iconInfo.materialIcon) { |
|
||||
return materialIconHtml(iconInfo.materialIcon); |
|
||||
} else { |
|
||||
return iconUrlHtml(iconInfo.iconUrl); |
|
||||
} |
|
||||
} else { |
|
||||
return ''; |
|
||||
} |
|
||||
} else { |
|
||||
return ''; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private datasourceToNode(datasource: HierarchyNodeDatasource, |
|
||||
data: DatasourceData[], |
|
||||
parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode { |
|
||||
const node: HierarchyNavTreeNode = { |
|
||||
id: (++this.nodeIdCounter) + '' |
|
||||
}; |
|
||||
this.nodesMap[node.id] = node; |
|
||||
datasource.nodeId = node.id; |
|
||||
node.icon = false; |
|
||||
const nodeCtx: HierarchyNodeContext = { |
|
||||
parentNodeCtx, |
|
||||
entity: { |
|
||||
id: { |
|
||||
id: datasource.entityId, |
|
||||
entityType: datasource.entityType |
|
||||
}, |
|
||||
name: datasource.entityName, |
|
||||
label: datasource.entityLabel ? datasource.entityLabel : datasource.entityName |
|
||||
}, |
|
||||
data: {} |
|
||||
}; |
|
||||
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, |
|
||||
filterId: datasource.filterId, |
|
||||
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 { |
|
||||
let relationQuery = this.nodeRelationQueryFunction(nodeCtx); |
|
||||
if (relationQuery && relationQuery === 'default') { |
|
||||
relationQuery = defaultNodeRelationQueryFunction(nodeCtx); |
|
||||
} |
|
||||
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; |
|
||||
} |
|
||||
} |
|
||||
@ -1,260 +0,0 @@ |
|||||
///
|
|
||||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|
||||
///
|
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||
/// you may not use this file except in compliance with the License.
|
|
||||
/// You may obtain a copy of the License at
|
|
||||
///
|
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||
///
|
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||
/// See the License for the specific language governing permissions and
|
|
||||
/// limitations under the License.
|
|
||||
///
|
|
||||
|
|
||||
import { BaseData } from '@shared/models/base-data'; |
|
||||
import { EntityId } from '@shared/models/id/entity-id'; |
|
||||
import { NavTreeNode } from '@shared/components/nav-tree.component'; |
|
||||
import { Datasource } from '@shared/models/widget.models'; |
|
||||
import { isDefined, isUndefined } from '@core/utils'; |
|
||||
import { EntityRelationsQuery, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models'; |
|
||||
import { EntityType } from '@shared/models/entity-type.models'; |
|
||||
import { Edge } from "@shared/models/edge.models"; |
|
||||
import { TranslateService } from "@ngx-translate/core"; |
|
||||
|
|
||||
export interface EdgeInstancesOverviewWidgetSettings { |
|
||||
nodeRelationQueryFunction: string; |
|
||||
nodeHasChildrenFunction: string; |
|
||||
nodeOpenedFunction: string; |
|
||||
nodeDisabledFunction: string; |
|
||||
nodeIconFunction: string; |
|
||||
nodeTextFunction: string; |
|
||||
nodesSortFunction: string; |
|
||||
} |
|
||||
|
|
||||
export interface HierarchyNodeContext { |
|
||||
parentNodeCtx?: HierarchyNodeContext; |
|
||||
entity: BaseData<EntityId>; |
|
||||
childrenNodesLoaded?: boolean; |
|
||||
level?: number; |
|
||||
data: {[key: string]: any}; |
|
||||
} |
|
||||
|
|
||||
export interface HierarchyNavTreeNode extends NavTreeNode { |
|
||||
data?: { |
|
||||
datasource: HierarchyNodeDatasource; |
|
||||
nodeCtx: HierarchyNodeContext; |
|
||||
searchText?: string; |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
export interface HierarchyNodeDatasource extends Datasource { |
|
||||
nodeId: string; |
|
||||
} |
|
||||
|
|
||||
export interface HierarchyNodeIconInfo { |
|
||||
iconUrl?: string; |
|
||||
materialIcon?: string; |
|
||||
} |
|
||||
|
|
||||
export type NodeRelationQueryFunction = (nodeCtx: HierarchyNodeContext) => EntityRelationsQuery | 'default'; |
|
||||
export type NodeTextFunction = (nodeCtx: HierarchyNodeContext) => string; |
|
||||
export type NodeDisabledFunction = (nodeCtx: HierarchyNodeContext) => boolean; |
|
||||
export type NodeIconFunction = (nodeCtx: HierarchyNodeContext) => HierarchyNodeIconInfo | 'default'; |
|
||||
export type NodeOpenedFunction = (nodeCtx: HierarchyNodeContext) => boolean; |
|
||||
export type NodeHasChildrenFunction = (nodeCtx: HierarchyNodeContext) => boolean; |
|
||||
export type NodesSortFunction = (nodeCtx1: HierarchyNodeContext, nodeCtx2: HierarchyNodeContext) => number; |
|
||||
|
|
||||
export function loadNodeCtxFunction<F extends (...args: any[]) => any>(functionBody: string, argNames: string, ...args: any[]): F { |
|
||||
let nodeCtxFunction: F = null; |
|
||||
if (isDefined(functionBody) && functionBody.length) { |
|
||||
try { |
|
||||
nodeCtxFunction = new Function(argNames, functionBody) as F; |
|
||||
const res = nodeCtxFunction.apply(null, args); |
|
||||
if (isUndefined(res)) { |
|
||||
nodeCtxFunction = null; |
|
||||
} |
|
||||
} catch (e) { |
|
||||
nodeCtxFunction = null; |
|
||||
} |
|
||||
} |
|
||||
return nodeCtxFunction; |
|
||||
} |
|
||||
|
|
||||
export function materialIconHtml(materialIcon: string): string { |
|
||||
return '<mat-icon class="node-icon material-icons" role="img" aria-hidden="false">' + materialIcon + '</mat-icon>'; |
|
||||
} |
|
||||
|
|
||||
export function iconUrlHtml(iconUrl: string): string { |
|
||||
return '<div class="node-icon" style="background-image: url(' + iconUrl + ');"> </div>'; |
|
||||
} |
|
||||
|
|
||||
export function edgeGroupsNodeText(translate: TranslateService, entityType: EntityType): string { |
|
||||
const nodeIcon = materialIconByEntityType(entityType); |
|
||||
const nodeText = textForEdgeGroupsType(translate, entityType); |
|
||||
return nodeIcon + nodeText; |
|
||||
} |
|
||||
|
|
||||
export function edgeNodeText(edge: Edge): string { |
|
||||
const nodeIcon = materialIconByEntityType(edge.id.entityType); |
|
||||
const nodeText = edge.name; |
|
||||
return nodeIcon + nodeText; |
|
||||
} |
|
||||
|
|
||||
export function materialIconByEntityType(entityType: EntityType): string { |
|
||||
let materialIcon = 'insert_drive_file'; |
|
||||
switch (entityType) { |
|
||||
case EntityType.DEVICE: |
|
||||
materialIcon = 'devices_other'; |
|
||||
break; |
|
||||
case EntityType.ASSET: |
|
||||
materialIcon = 'domain'; |
|
||||
break; |
|
||||
case EntityType.CUSTOMER: |
|
||||
materialIcon = 'supervisor_account'; |
|
||||
break; |
|
||||
case EntityType.USER: |
|
||||
materialIcon = 'account_circle'; |
|
||||
break; |
|
||||
case EntityType.DASHBOARD: |
|
||||
materialIcon = 'dashboards'; |
|
||||
break; |
|
||||
case EntityType.ENTITY_VIEW: |
|
||||
materialIcon = 'view_quilt'; |
|
||||
break; |
|
||||
case EntityType.RULE_CHAIN: |
|
||||
materialIcon = 'settings_ethernet'; |
|
||||
break; |
|
||||
case EntityType.EDGE: |
|
||||
materialIcon = 'router'; |
|
||||
break; |
|
||||
} |
|
||||
return '<mat-icon class="node-icon material-icons" role="img" aria-hidden="false">' + materialIcon + '</mat-icon>'; |
|
||||
} |
|
||||
|
|
||||
export function textForEdgeGroupsType(translate: TranslateService, entityType: EntityType): string { |
|
||||
let textForEdgeGroupsType: string = ''; |
|
||||
switch (entityType) { |
|
||||
case EntityType.DEVICE: |
|
||||
textForEdgeGroupsType = 'device.devices'; |
|
||||
break; |
|
||||
case EntityType.ASSET: |
|
||||
textForEdgeGroupsType = 'asset.assets'; |
|
||||
break; |
|
||||
case EntityType.DASHBOARD: |
|
||||
textForEdgeGroupsType = 'dashboard.dashboards'; |
|
||||
break; |
|
||||
case EntityType.ENTITY_VIEW: |
|
||||
textForEdgeGroupsType = 'entity-view.entity-views'; |
|
||||
break; |
|
||||
case EntityType.RULE_CHAIN: |
|
||||
textForEdgeGroupsType = 'rulechain.rulechains'; |
|
||||
break; |
|
||||
} |
|
||||
return translate.instant(textForEdgeGroupsType); |
|
||||
} |
|
||||
|
|
||||
export const defaultNodeRelationQueryFunction: NodeRelationQueryFunction = nodeCtx => { |
|
||||
const entity = nodeCtx.entity; |
|
||||
const query: EntityRelationsQuery = { |
|
||||
parameters: { |
|
||||
rootId: entity.id.id, |
|
||||
rootType: entity.id.entityType as EntityType, |
|
||||
direction: EntitySearchDirection.FROM, |
|
||||
relationTypeGroup: RelationTypeGroup.COMMON, |
|
||||
maxLevel: 1 |
|
||||
}, |
|
||||
filters: [ |
|
||||
{ |
|
||||
relationType: 'Contains', |
|
||||
entityTypes: [] |
|
||||
} |
|
||||
] |
|
||||
}; |
|
||||
return query; |
|
||||
}; |
|
||||
|
|
||||
export const edgeGroupsTypes: EntityType[] = [ |
|
||||
EntityType.ASSET, |
|
||||
EntityType.DEVICE, |
|
||||
EntityType.ENTITY_VIEW, |
|
||||
EntityType.DASHBOARD, |
|
||||
EntityType.RULE_CHAIN |
|
||||
] |
|
||||
|
|
||||
export const defaultNodeIconFunction: NodeIconFunction = nodeCtx => { |
|
||||
let materialIcon = 'insert_drive_file'; |
|
||||
const entity = nodeCtx.entity; |
|
||||
if (entity && entity.id && entity.id.entityType) { |
|
||||
switch (entity.id.entityType as EntityType | string) { |
|
||||
case 'function': |
|
||||
materialIcon = 'functions'; |
|
||||
break; |
|
||||
case EntityType.DEVICE: |
|
||||
materialIcon = 'devices_other'; |
|
||||
break; |
|
||||
case EntityType.ASSET: |
|
||||
materialIcon = 'domain'; |
|
||||
break; |
|
||||
case EntityType.TENANT: |
|
||||
materialIcon = 'supervisor_account'; |
|
||||
break; |
|
||||
case EntityType.CUSTOMER: |
|
||||
materialIcon = 'supervisor_account'; |
|
||||
break; |
|
||||
case EntityType.USER: |
|
||||
materialIcon = 'account_circle'; |
|
||||
break; |
|
||||
case EntityType.DASHBOARD: |
|
||||
materialIcon = 'dashboards'; |
|
||||
break; |
|
||||
case EntityType.ALARM: |
|
||||
materialIcon = 'notifications_active'; |
|
||||
break; |
|
||||
case EntityType.ENTITY_VIEW: |
|
||||
materialIcon = 'view_quilt'; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
return { |
|
||||
materialIcon |
|
||||
}; |
|
||||
}; |
|
||||
|
|
||||
export const defaultNodeOpenedFunction: NodeOpenedFunction = nodeCtx => { |
|
||||
return nodeCtx.level <= 4; |
|
||||
}; |
|
||||
|
|
||||
export const defaultNodesSortFunction: NodesSortFunction = (nodeCtx1, nodeCtx2) => { |
|
||||
let result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); |
|
||||
if (result === 0) { |
|
||||
result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name); |
|
||||
} |
|
||||
return result; |
|
||||
}; |
|
||||
|
|
||||
export interface EdgeInstancesOverviewNode extends NavTreeNode { |
|
||||
data?: EdgeInstancesOverviewNodeData; |
|
||||
} |
|
||||
|
|
||||
export type EdgeInstancesOverviewNodeData = EdgeGroupsNodeData | EdgeNodeData; |
|
||||
|
|
||||
export interface EdgeGroupsNodeData extends BaseEdgeInstancesOverviewNodeData { |
|
||||
type: 'edgeGroups'; |
|
||||
entityType: EntityType; |
|
||||
edge: Edge; |
|
||||
} |
|
||||
|
|
||||
export interface EdgeNodeData extends BaseEdgeInstancesOverviewNodeData { |
|
||||
type: 'edge'; |
|
||||
entity: Edge; |
|
||||
} |
|
||||
|
|
||||
export interface BaseEdgeInstancesOverviewNodeData { |
|
||||
type: EdgeInstancesOverviewNodeType; |
|
||||
internalId: string; |
|
||||
} |
|
||||
|
|
||||
export type EdgeInstancesOverviewNodeType = 'edge' | 'edgeGroups'; |
|
||||
Loading…
Reference in new issue