9 changed files with 1044 additions and 1 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,51 @@ |
|||
<!-- |
|||
|
|||
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> |
|||
@ -0,0 +1,121 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
:host-context(.tb-has-timewindow) { |
|||
.tb-edge-instances-overview { |
|||
mat-toolbar { |
|||
height: 60px; |
|||
max-height: 60px; |
|||
.mat-toolbar-tools { |
|||
height: 60px; |
|||
max-height: 60px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host { |
|||
.tb-edge-instances-overview { |
|||
mat-toolbar.mat-table-toolbar:not([color="primary"]) { |
|||
background: transparent; |
|||
} |
|||
mat-toolbar { |
|||
min-height: 39px; |
|||
max-height: 39px; |
|||
.mat-toolbar-tools { |
|||
min-height: 39px; |
|||
max-height: 39px; |
|||
} |
|||
} |
|||
|
|||
.tb-edges-nav-tree-panel { |
|||
overflow-x: auto; |
|||
overflow-y: auto; |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.tb-nav-tree-container { |
|||
&.jstree-proton { |
|||
.jstree-anchor { |
|||
div.node-icon { |
|||
display: inline-block; |
|||
width: 22px; |
|||
height: 22px; |
|||
margin-right: 2px; |
|||
margin-bottom: 2px; |
|||
background-color: transparent; |
|||
background-repeat: no-repeat; |
|||
background-attachment: scroll; |
|||
background-position: center center; |
|||
background-size: 18px 18px; |
|||
} |
|||
|
|||
mat-icon.node-icon { |
|||
width: 22px; |
|||
min-width: 22px; |
|||
height: 22px; |
|||
min-height: 22px; |
|||
margin-right: 2px; |
|||
margin-bottom: 2px; |
|||
color: inherit; |
|||
vertical-align: middle; |
|||
|
|||
&.material-icons { |
|||
font-size: 18px; |
|||
line-height: 22px; |
|||
text-align: center; |
|||
} |
|||
} |
|||
|
|||
&.jstree-hovered:not(.jstree-clicked), |
|||
&.jstree-disabled { |
|||
div.node-icon { |
|||
opacity: .5; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 768px) { |
|||
.tb-nav-tree-container { |
|||
&.jstree-proton-responsive { |
|||
.jstree-anchor { |
|||
div.node-icon { |
|||
width: 40px; |
|||
height: 40px; |
|||
margin: 0; |
|||
background-size: 24px 24px; |
|||
} |
|||
|
|||
mat-icon.node-icon { |
|||
width: 40px; |
|||
min-width: 40px; |
|||
height: 40px; |
|||
min-height: 40px; |
|||
margin: 0; |
|||
|
|||
&.material-icons { |
|||
font-size: 24px; |
|||
line-height: 40px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,554 @@ |
|||
///
|
|||
/// 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; |
|||
} |
|||
} |
|||
@ -0,0 +1,260 @@ |
|||
///
|
|||
/// 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