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