26 changed files with 1594 additions and 26 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,51 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 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-entities-hierarchy 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-entities-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,122 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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-entities-hierarchy { |
|||
mat-toolbar { |
|||
height: 60px; |
|||
max-height: 60px; |
|||
.mat-toolbar-tools { |
|||
height: 60px; |
|||
max-height: 60px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host { |
|||
.tb-entities-hierarchy { |
|||
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-entities-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,490 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 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 { forkJoin, fromEvent, Observable, of } from 'rxjs'; |
|||
import { catchError, debounceTime, distinctUntilChanged, map, mergeMap, tap } from 'rxjs/operators'; |
|||
import { constructTableCssString } from '@home/components/widget/lib/table-widget.models'; |
|||
import { Overlay } from '@angular/cdk/overlay'; |
|||
import { |
|||
LoadNodesCallback, |
|||
NavTreeEditCallbacks, |
|||
NodeSearchCallback, |
|||
NodeSelectedCallback, |
|||
NodesInsertedCallback |
|||
} from '@shared/components/nav-tree.component'; |
|||
import { BaseData } from '@shared/models/base-data'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { |
|||
defaultNodeIconFunction, |
|||
defaultNodeOpenedFunction, |
|||
defaultNodeRelationQueryFunction, |
|||
defaultNodesSortFunction, |
|||
EntitiesHierarchyWidgetSettings, |
|||
HierarchyNavTreeNode, |
|||
HierarchyNodeContext, |
|||
HierarchyNodeDatasource, |
|||
iconUrlHtml, |
|||
loadNodeCtxFunction, |
|||
materialIconHtml, |
|||
NodeDisabledFunction, |
|||
NodeHasChildrenFunction, |
|||
NodeIconFunction, |
|||
NodeOpenedFunction, |
|||
NodeRelationQueryFunction, |
|||
NodesSortFunction, |
|||
NodeTextFunction |
|||
} from '@home/components/widget/lib/entities-hierarchy-widget.models'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { EntityRelationsQuery, EntitySearchDirection } from '@shared/models/relation.models'; |
|||
import { EntityRelationService } from '@core/http/entity-relation.service'; |
|||
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-entities-hierarchy-widget', |
|||
templateUrl: './entities-hierarchy-widget.component.html', |
|||
styleUrls: ['./entities-hierarchy-widget.component.scss'] |
|||
}) |
|||
export class EntitiesHierarchyWidgetComponent extends PageComponent implements OnInit, AfterViewInit { |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef; |
|||
|
|||
public toastTargetId = 'entities-hierarchy-' + this.utils.guid(); |
|||
|
|||
public textSearchMode = false; |
|||
public textSearch = null; |
|||
|
|||
public nodeEditCallbacks: NavTreeEditCallbacks = {}; |
|||
|
|||
private settings: EntitiesHierarchyWidgetSettings; |
|||
private widgetConfig: WidgetConfig; |
|||
private subscription: IWidgetSubscription; |
|||
private datasources: Array<HierarchyNodeDatasource>; |
|||
|
|||
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 searchAction: WidgetAction = { |
|||
name: 'action.search', |
|||
show: true, |
|||
icon: 'search', |
|||
onAction: () => { |
|||
this.enterFilterMode(); |
|||
} |
|||
}; |
|||
|
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private elementRef: ElementRef, |
|||
private overlay: Overlay, |
|||
private viewContainerRef: ViewContainerRef, |
|||
private utils: UtilsService, |
|||
private entityService: EntityService, |
|||
private entityRelationService: EntityRelationService) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.ctx.$scope.entitiesHierarchyWidget = 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.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 = 'entities-hierarchy-' + this.utils.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 = undefined; |
|||
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 tasks: Observable<HierarchyNavTreeNode>[] = []; |
|||
this.datasources.forEach((datasource) => { |
|||
tasks.push(this.datasourceToNode(datasource)); |
|||
}); |
|||
forkJoin(tasks).subscribe((nodes) => { |
|||
cb(this.prepareNodes(nodes)); |
|||
this.updateNodeData(this.subscription.data); |
|||
}); |
|||
} else { |
|||
if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.nodeCtx.entity.id.entityType !== 'function') { |
|||
const relationQuery = this.prepareNodeRelationQuery(node.data.nodeCtx); |
|||
this.entityRelationService.findByQuery(relationQuery, {ignoreErrors: true, ignoreLoading: true}).subscribe( |
|||
(entityRelations) => { |
|||
if (entityRelations.length) { |
|||
const tasks: Observable<HierarchyNavTreeNode>[] = []; |
|||
entityRelations.forEach((relation) => { |
|||
const targetId = relationQuery.parameters.direction === EntitySearchDirection.FROM ? relation.to : relation.from; |
|||
tasks.push(this.entityIdToNode(targetId.entityType as EntityType, targetId.id, node.data.datasource, node.data.nodeCtx)); |
|||
}); |
|||
forkJoin(tasks).subscribe((nodes) => { |
|||
cb(this.prepareNodes(nodes)); |
|||
}); |
|||
} else { |
|||
cb([]); |
|||
} |
|||
}, |
|||
(error) => { |
|||
let errorText = 'Failed to get relations!'; |
|||
if (error && error.status === 400) { |
|||
errorText = 'Invalid relations query returned by \'Node relations query function\'! Please check widget configuration!'; |
|||
} |
|||
this.showError(errorText); |
|||
} |
|||
); |
|||
} else { |
|||
cb([]); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
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, parent) => { |
|||
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 showError(errorText: string) { |
|||
this.store.dispatch(new ActionNotificationShow( |
|||
{ |
|||
message: errorText, |
|||
type: 'error', |
|||
target: this.toastTargetId, |
|||
verticalPosition: 'bottom', |
|||
horizontalPosition: 'left' |
|||
})); |
|||
} |
|||
|
|||
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, parentNodeCtx?: HierarchyNodeContext): Observable<HierarchyNavTreeNode> { |
|||
return this.resolveEntity(datasource).pipe( |
|||
map(entity => { |
|||
if (entity !== null) { |
|||
const node: HierarchyNavTreeNode = { |
|||
id: (++this.nodeIdCounter)+'' |
|||
}; |
|||
this.nodesMap[node.id] = node; |
|||
datasource.nodeId = node.id; |
|||
node.icon = false; |
|||
const nodeCtx: HierarchyNodeContext = { |
|||
parentNodeCtx, |
|||
entity, |
|||
data: {} |
|||
}; |
|||
nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1; |
|||
node.data = { |
|||
datasource, |
|||
nodeCtx |
|||
}; |
|||
node.state = { |
|||
disabled: this.nodeDisabledFunction(node.data.nodeCtx), |
|||
opened: this.nodeOpenedFunction(node.data.nodeCtx) |
|||
}; |
|||
node.text = this.prepareNodeText(node); |
|||
node.children = this.nodeHasChildrenFunction(node.data.nodeCtx); |
|||
return node; |
|||
} else { |
|||
return null; |
|||
} |
|||
}) |
|||
); |
|||
} |
|||
|
|||
private entityIdToNode(entityType: EntityType, entityId: string, |
|||
parentDatasource: HierarchyNodeDatasource, |
|||
parentNodeCtx: HierarchyNodeContext): Observable<HierarchyNavTreeNode> { |
|||
const datasource = { |
|||
dataKeys: parentDatasource.dataKeys, |
|||
type: DatasourceType.entity, |
|||
entityType, |
|||
entityId |
|||
} as HierarchyNodeDatasource; |
|||
return this.datasourceToNode(datasource, parentNodeCtx).pipe( |
|||
mergeMap((node) => { |
|||
if (node != null) { |
|||
const subscriptionOptions: WidgetSubscriptionOptions = { |
|||
type: widgetType.latest, |
|||
datasources: [datasource], |
|||
callbacks: { |
|||
onDataUpdated: subscription => { |
|||
this.updateNodeData(subscription.data); |
|||
} |
|||
} |
|||
}; |
|||
return this.ctx.subscriptionApi. |
|||
createSubscription(subscriptionOptions, true).pipe( |
|||
map(() => node)); |
|||
} else { |
|||
return of(node); |
|||
} |
|||
}) |
|||
); |
|||
} |
|||
|
|||
private resolveEntity(datasource: HierarchyNodeDatasource): Observable<BaseData<EntityId>> { |
|||
if (datasource.type === DatasourceType.function) { |
|||
const entity = { |
|||
id: { |
|||
entityType: 'function' |
|||
}, |
|||
name: datasource.name |
|||
}; |
|||
return of(entity as BaseData<EntityId>); |
|||
} else { |
|||
return this.entityService.getEntity(datasource.entityType, datasource.entityId, {ignoreLoading: true}).pipe( |
|||
catchError(err => of(null)) |
|||
); |
|||
} |
|||
} |
|||
|
|||
private prepareNodeRelationQuery(nodeCtx: HierarchyNodeContext): EntityRelationsQuery { |
|||
let relationQuery = this.nodeRelationQueryFunction(nodeCtx); |
|||
if (relationQuery && relationQuery === 'default') { |
|||
relationQuery = defaultNodeRelationQueryFunction(nodeCtx); |
|||
} |
|||
return relationQuery as EntityRelationsQuery; |
|||
} |
|||
} |
|||
@ -0,0 +1,160 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 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'; |
|||
|
|||
export interface EntitiesHierarchyWidgetSettings { |
|||
nodeRelationQueryFunction: string; |
|||
nodeHasChildrenFunction: string; |
|||
nodeOpenedFunction: string; |
|||
nodeDisabledFunction: string; |
|||
nodeIconFunction: string; |
|||
nodeTextFunction: string; |
|||
nodesSortFunction: string; |
|||
} |
|||
|
|||
export interface HierarchyNodeContext { |
|||
parentNodeCtx?: HierarchyNodeContext; |
|||
entity: BaseData<EntityId>; |
|||
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 Function>(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 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 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; |
|||
}; |
|||
@ -0,0 +1,18 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 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-nav-tree-container"></div> |
|||
@ -0,0 +1,347 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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. |
|||
*/ |
|||
.tb-nav-tree-container { |
|||
padding: 15px; |
|||
font-family: Roboto, "Helvetica Neue", sans-serif; |
|||
|
|||
&.jstree-proton { |
|||
.jstree-node, |
|||
.jstree-icon { |
|||
background-image: url("../../../assets/jstree/32px.png"); |
|||
} |
|||
|
|||
.jstree-last { |
|||
background: transparent; |
|||
} |
|||
|
|||
.jstree-themeicon-custom { |
|||
background-image: none; |
|||
|
|||
&.material-icons { |
|||
font-size: 18px; |
|||
} |
|||
} |
|||
|
|||
.jstree-anchor { |
|||
font-size: 16px; |
|||
} |
|||
} |
|||
|
|||
&.jstree-proton-small { |
|||
.jstree-node, |
|||
.jstree-icon { |
|||
background-image: url("../../../assets/jstree/32px.png"); |
|||
} |
|||
|
|||
.jstree-last { |
|||
background: transparent; |
|||
} |
|||
|
|||
.jstree-themeicon-custom { |
|||
background-image: none; |
|||
|
|||
&.material-icons { |
|||
font-size: 14px; |
|||
} |
|||
} |
|||
|
|||
.jstree-anchor { |
|||
font-size: 14px; |
|||
} |
|||
} |
|||
|
|||
&.jstree-proton-large { |
|||
.jstree-node, |
|||
.jstree-icon { |
|||
background-image: url("../../../assets/jstree/32px.png"); |
|||
} |
|||
|
|||
.jstree-last { |
|||
background: transparent; |
|||
} |
|||
|
|||
.jstree-themeicon-custom { |
|||
background-image: none; |
|||
|
|||
&.material-icons { |
|||
font-size: 24px; |
|||
} |
|||
} |
|||
|
|||
.jstree-anchor { |
|||
font-size: 20px; |
|||
} |
|||
} |
|||
|
|||
a { |
|||
border-bottom: none; |
|||
|
|||
i.jstree-themeicon-custom { |
|||
&.tb-user-group { |
|||
&::before { |
|||
content: "account_circle"; |
|||
} |
|||
} |
|||
|
|||
&.tb-customer-group { |
|||
&::before { |
|||
content: "supervisor_account"; |
|||
} |
|||
} |
|||
|
|||
&.tb-asset-group { |
|||
&::before { |
|||
content: "domain"; |
|||
} |
|||
} |
|||
|
|||
&.tb-device-group { |
|||
&::before { |
|||
content: "devices_other"; |
|||
} |
|||
} |
|||
|
|||
&.tb-entity-view-group { |
|||
&::before { |
|||
content: "view_quilt"; |
|||
} |
|||
} |
|||
|
|||
&.tb-dashboard-group { |
|||
&::before { |
|||
content: "dashboard"; |
|||
} |
|||
} |
|||
|
|||
&.tb-customer { |
|||
&::before { |
|||
content: "supervisor_account"; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 768px) { |
|||
.tb-nav-tree-container { |
|||
&.jstree-proton-responsive { |
|||
.jstree-node, |
|||
.jstree-icon, |
|||
.jstree-node > .jstree-ocl, |
|||
.jstree-themeicon, |
|||
.jstree-checkbox { |
|||
background-image: url("../../../assets/jstree/40px.png"); |
|||
background-size: 120px 240px; |
|||
} |
|||
|
|||
.jstree-container-ul { |
|||
overflow: visible; |
|||
} |
|||
|
|||
.jstree-themeicon-custom { |
|||
background-color: transparent; |
|||
background-image: none; |
|||
background-position: 0 0; |
|||
|
|||
&.material-icons { |
|||
margin: 0; |
|||
font-size: 24px; |
|||
} |
|||
} |
|||
|
|||
.jstree-node, |
|||
.jstree-leaf > .jstree-ocl { |
|||
background: 0 0; |
|||
} |
|||
|
|||
.jstree-node { |
|||
min-width: 40px; |
|||
min-height: 40px; |
|||
margin-left: 40px; |
|||
line-height: 40px; |
|||
white-space: nowrap; |
|||
background-repeat: repeat-y; |
|||
background-position: -80px 0; |
|||
} |
|||
|
|||
.jstree-last { |
|||
background: 0 0; |
|||
} |
|||
|
|||
.jstree-anchor { |
|||
height: 40px; |
|||
font-size: 1.1em; |
|||
font-weight: 700; |
|||
line-height: 40px; |
|||
text-shadow: 1px 1px #fff; |
|||
} |
|||
|
|||
.jstree-icon, |
|||
.jstree-icon:empty { |
|||
width: 40px; |
|||
height: 40px; |
|||
line-height: 40px; |
|||
} |
|||
|
|||
> { |
|||
.jstree-container-ul > .jstree-node { |
|||
margin-right: 0; |
|||
margin-left: 0; |
|||
} |
|||
} |
|||
|
|||
.jstree-ocl, |
|||
.jstree-themeicon, |
|||
.jstree-checkbox { |
|||
background-size: 120px 240px; |
|||
} |
|||
|
|||
.jstree-leaf > .jstree-ocl { |
|||
background: 0 0; |
|||
background-position: -40px -120px; |
|||
} |
|||
|
|||
.jstree-last > .jstree-ocl { |
|||
background-position: -40px -160px; |
|||
} |
|||
|
|||
.jstree-open > .jstree-ocl { |
|||
background-position: 0 0 !important; |
|||
} |
|||
|
|||
.jstree-closed > .jstree-ocl { |
|||
background-position: 0 -40px !important; |
|||
} |
|||
|
|||
.jstree-themeicon { |
|||
background-position: -40px -40px; |
|||
} |
|||
|
|||
.jstree-checkbox, |
|||
.jstree-checkbox:hover { |
|||
background-position: -40px -80px; |
|||
} |
|||
|
|||
&.jstree-checkbox-selection { |
|||
.jstree-clicked > .jstree-checkbox, |
|||
.jstree-clicked > .jstree-checkbox:hover { |
|||
background-position: 0 -80px; |
|||
} |
|||
} |
|||
|
|||
.jstree-checked > .jstree-checkbox, |
|||
.jstree-checked > .jstree-checkbox:hover { |
|||
background-position: 0 -80px; |
|||
} |
|||
|
|||
.jstree-anchor > .jstree-undetermined, |
|||
.jstree-anchor > .jstree-undetermined:hover { |
|||
background-position: 0 -120px; |
|||
} |
|||
|
|||
.jstree-striped { |
|||
background: 0 0; |
|||
} |
|||
|
|||
.jstree-wholerow { |
|||
height: 40px; |
|||
background: #ebebeb; |
|||
border-top: 1px solid rgba(255, 255, 255, .7); |
|||
border-bottom: 1px solid rgba(64, 64, 64, .2); |
|||
} |
|||
|
|||
.jstree-wholerow-hovered { |
|||
background: #e7f4f9; |
|||
} |
|||
|
|||
.jstree-wholerow-clicked { |
|||
background: #beebff; |
|||
} |
|||
|
|||
.jstree-children { |
|||
.jstree-last > .jstree-wholerow { |
|||
box-shadow: inset 0 -6px 3px -5px #666; |
|||
} |
|||
|
|||
.jstree-open > .jstree-wholerow { |
|||
border-top: 0; |
|||
box-shadow: inset 0 6px 3px -5px #666; |
|||
} |
|||
|
|||
.jstree-open + .jstree-open { |
|||
box-shadow: none; |
|||
} |
|||
} |
|||
|
|||
&.jstree-rtl { |
|||
.jstree-node { |
|||
margin-right: 40px; |
|||
margin-left: 0; |
|||
} |
|||
|
|||
.jstree-container-ul > .jstree-node { |
|||
margin-right: 0; |
|||
} |
|||
|
|||
.jstree-closed > .jstree-ocl { |
|||
background-position: -40px 0 !important; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-nav-tree .mat-button.tb-active { |
|||
font-weight: 500; |
|||
background-color: rgba(255, 255, 255, .15); |
|||
} |
|||
|
|||
.tb-nav-tree, |
|||
.tb-nav-tree ul { |
|||
margin-top: 0; |
|||
list-style: none; |
|||
|
|||
&:first-child { |
|||
padding: 0; |
|||
} |
|||
|
|||
li { |
|||
.mat-button { |
|||
width: 100%; |
|||
max-height: 40px; |
|||
padding: 0 16px; |
|||
margin: 0; |
|||
overflow: hidden; |
|||
line-height: 40px; |
|||
color: inherit; |
|||
text-align: left; |
|||
text-decoration: none; |
|||
text-overflow: ellipsis; |
|||
text-transform: none; |
|||
text-rendering: optimizeLegibility; |
|||
white-space: nowrap; |
|||
cursor: pointer; |
|||
border-radius: 0; |
|||
|
|||
span { |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,272 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 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 { Component, ElementRef, Input, NgZone, OnInit, ViewEncapsulation } from '@angular/core'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { deepClone } from '@core/utils'; |
|||
|
|||
export interface NavTreeNodeState { |
|||
disabled?: boolean; |
|||
opened?: boolean; |
|||
loaded?: boolean; |
|||
} |
|||
|
|||
export interface NavTreeNode { |
|||
id: string; |
|||
icon?: boolean; |
|||
text?: string; |
|||
state?: NavTreeNodeState; |
|||
children?: NavTreeNode[] | boolean; |
|||
data?: any; |
|||
} |
|||
|
|||
export interface NavTreeEditCallbacks { |
|||
selectNode?: (id: string) => void; |
|||
deselectAll?: () => void; |
|||
getNode?: (id: string) => NavTreeNode; |
|||
getParentNodeId?: (id: string) => string; |
|||
openNode?: (id: string, cb?: () => void) => void; |
|||
nodeIsOpen?: (id: string) => boolean; |
|||
nodeIsLoaded?: (id: string) => boolean; |
|||
refreshNode?: (id: string) => void; |
|||
updateNode?: (id: string, newName: string) => void; |
|||
createNode?: (parentId: string, node: NavTreeNode, pos: number) => void; |
|||
deleteNode?: (id: string) => void; |
|||
disableNode?: (id: string) => void; |
|||
enableNode?: (id: string) => void; |
|||
setNodeHasChildren?: (id: string, hasChildren: boolean) => void; |
|||
search?: (searchText: string) => void; |
|||
clearSearch?: () => void; |
|||
} |
|||
|
|||
export type NodesCallback = (nodes: NavTreeNode[]) => void; |
|||
export type LoadNodesCallback = (node: NavTreeNode, cb: NodesCallback) => void; |
|||
export type NodeSearchCallback = (searchText: string, node: NavTreeNode) => boolean; |
|||
export type NodeSelectedCallback = (node: NavTreeNode, event: Event) => void; |
|||
export type NodesInsertedCallback = (nodes: string[], parent: string) => void; |
|||
|
|||
@Component({ |
|||
selector: 'tb-nav-tree', |
|||
templateUrl: './nav-tree.component.html', |
|||
styleUrls: ['./nav-tree.component.scss'], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class NavTreeComponent implements OnInit { |
|||
|
|||
private enableSearchValue: boolean; |
|||
get enableSearch(): boolean { |
|||
return this.enableSearchValue; |
|||
} |
|||
@Input() |
|||
set enableSearch(value: boolean) { |
|||
this.enableSearchValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
@Input() |
|||
private loadNodes: LoadNodesCallback; |
|||
|
|||
@Input() |
|||
private searchCallback: NodeSearchCallback; |
|||
|
|||
@Input() |
|||
private onNodeSelected: NodeSelectedCallback; |
|||
|
|||
@Input() |
|||
private onNodesInserted: NodesInsertedCallback; |
|||
|
|||
@Input() |
|||
private editCallbacks: NavTreeEditCallbacks; |
|||
|
|||
private treeElement: JSTree; |
|||
|
|||
constructor(private elementRef: ElementRef<HTMLElement>, |
|||
private ngZone: NgZone) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.initTree(); |
|||
} |
|||
|
|||
private initTree() { |
|||
|
|||
const loadNodes: LoadNodesCallback = (node, cb) => { |
|||
const outCb = (_nodes: NavTreeNode[]) => { |
|||
const copied: NavTreeNode[] = []; |
|||
if (_nodes) { |
|||
_nodes.forEach((n) => { |
|||
copied.push(deepClone(n, ['data'])); |
|||
}); |
|||
} |
|||
cb(copied); |
|||
}; |
|||
this.ngZone.runOutsideAngular(() => { |
|||
this.loadNodes(node, outCb); |
|||
}); |
|||
}; |
|||
|
|||
const config: JSTreeStaticDefaults = { |
|||
core: { |
|||
worker: false, |
|||
multiple: false, |
|||
check_callback: true, |
|||
themes: { name: 'proton', responsive: true }, |
|||
data: loadNodes, |
|||
error: () => { |
|||
console.error('Unexpected jstree error!'); |
|||
} |
|||
}, |
|||
plugins: [] |
|||
}; |
|||
|
|||
if (this.enableSearch) { |
|||
config.plugins.push('search'); |
|||
config.search = { |
|||
ajax: false, |
|||
fuzzy: false, |
|||
close_opened_onclear: true, |
|||
case_sensitive: false, |
|||
show_only_matches: true, |
|||
show_only_matches_children: false, |
|||
search_leaves_only: false, |
|||
search_callback: this.searchCallback |
|||
}; |
|||
} |
|||
|
|||
this.treeElement = $('.tb-nav-tree-container', this.elementRef.nativeElement).jstree(config); |
|||
|
|||
this.treeElement.on('changed.jstree', (e, data) => { |
|||
const node: NavTreeNode = data.instance.get_selected(true)[0]; |
|||
if (this.onNodeSelected) { |
|||
this.onNodeSelected(node, e); |
|||
} |
|||
}); |
|||
|
|||
this.treeElement.on('model.jstree', (e, data) => { |
|||
if (this.onNodesInserted) { |
|||
this.onNodesInserted(data.nodes, data.parent); |
|||
} |
|||
}); |
|||
|
|||
if (this.editCallbacks) { |
|||
this.editCallbacks.selectNode = id => { |
|||
const node: NavTreeNode = this.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
this.treeElement.jstree('deselect_all', true); |
|||
this.treeElement.jstree('select_node', node); |
|||
} |
|||
}; |
|||
this.editCallbacks.deselectAll = () => { |
|||
this.treeElement.jstree('deselect_all'); |
|||
}; |
|||
this.editCallbacks.getNode = (id) => { |
|||
const node: NavTreeNode = this.treeElement.jstree('get_node', id); |
|||
return node; |
|||
}; |
|||
this.editCallbacks.getParentNodeId = (id) => { |
|||
const node: NavTreeNode = this.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
return this.treeElement.jstree('get_parent', node); |
|||
} |
|||
}; |
|||
this.editCallbacks.openNode = (id, cb) => { |
|||
const node: NavTreeNode = this.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
this.treeElement.jstree('open_node', node, cb); |
|||
} |
|||
}; |
|||
this.editCallbacks.nodeIsOpen = (id) => { |
|||
const node: NavTreeNode = this.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
return this.treeElement.jstree('is_open', node); |
|||
} else { |
|||
return true; |
|||
} |
|||
}; |
|||
this.editCallbacks.nodeIsLoaded = (id) => { |
|||
const node: NavTreeNode = this.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
return this.treeElement.jstree('is_loaded', node); |
|||
} else { |
|||
return true; |
|||
} |
|||
}; |
|||
this.editCallbacks.refreshNode = (id) => { |
|||
if (id === '#') { |
|||
this.treeElement.jstree('refresh'); |
|||
this.treeElement.jstree('redraw'); |
|||
} else { |
|||
const node: NavTreeNode = this.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
const opened = this.treeElement.jstree('is_open', node); |
|||
this.treeElement.jstree('refresh_node', node); |
|||
this.treeElement.jstree('redraw'); |
|||
if (node.children && opened/* && !node.children.length*/) { |
|||
this.treeElement.jstree('open_node', node); |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
this.editCallbacks.updateNode = (id, newName) => { |
|||
const node: NavTreeNode = this.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
this.treeElement.jstree('rename_node', node, newName); |
|||
} |
|||
}; |
|||
this.editCallbacks.createNode = (parentId, node, pos) => { |
|||
const parentNode: NavTreeNode = this.treeElement.jstree('get_node', parentId); |
|||
if (parentNode) { |
|||
this.treeElement.jstree('create_node', parentNode, node, pos); |
|||
} |
|||
}; |
|||
this.editCallbacks.deleteNode = (id) => { |
|||
const node: NavTreeNode = this.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
this.treeElement.jstree('delete_node', node); |
|||
} |
|||
}; |
|||
this.editCallbacks.disableNode = (id) => { |
|||
const node: NavTreeNode = this.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
this.treeElement.jstree('disable_node', node); |
|||
} |
|||
}; |
|||
this.editCallbacks.enableNode = (id) => { |
|||
const node: NavTreeNode = this.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
this.treeElement.jstree('enable_node', node); |
|||
} |
|||
}; |
|||
this.editCallbacks.setNodeHasChildren = (id, hasChildren) => { |
|||
const node: NavTreeNode = this.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
if (!node.children || (Array.isArray(node.children) && !node.children.length)) { |
|||
node.children = hasChildren; |
|||
node.state.loaded = !hasChildren; |
|||
node.state.opened = false; |
|||
this.treeElement.jstree('_node_changed', node.id); |
|||
this.treeElement.jstree('redraw'); |
|||
} |
|||
} |
|||
}; |
|||
this.editCallbacks.search = (searchText) => { |
|||
this.treeElement.jstree('search', searchText); |
|||
}; |
|||
this.editCallbacks.clearSearch = () => { |
|||
this.treeElement.jstree('clear_search'); |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 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.
|
|||
///
|
|||
|
|||
interface JSTreeEventData { |
|||
instance: JSTree; |
|||
} |
|||
|
|||
interface JSTreeModelEventData extends JSTreeEventData { |
|||
nodes: string[]; |
|||
parent: string; |
|||
} |
|||
|
|||
interface JQuery { |
|||
on(events: 'changed.jstree', handler: (e: Event, data: JSTreeEventData) => void): this; |
|||
on(events: 'model.jstree', handler: (e: Event, data: JSTreeModelEventData) => void): this; |
|||
} |
|||
Loading…
Reference in new issue