16 changed files with 1189 additions and 7 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,185 @@ |
|||
/* |
|||
* 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 './nav-tree.scss'; |
|||
|
|||
/* eslint-disable import/no-unresolved, import/default */ |
|||
|
|||
import navTreeTemplate from './nav-tree.tpl.html'; |
|||
|
|||
/* eslint-enable import/no-unresolved, import/default */ |
|||
|
|||
export default angular.module('thingsboard.directives.navTree', []) |
|||
.directive('tbNavTree', NavTree) |
|||
.name; |
|||
|
|||
/*@ngInject*/ |
|||
function NavTree() { |
|||
return { |
|||
restrict: "E", |
|||
scope: true, |
|||
bindToController: { |
|||
loadNodes: '=', |
|||
editCallbacks: '=', |
|||
onNodeSelected: '&', |
|||
onNodesInserted: '&' |
|||
}, |
|||
controller: NavTreeController, |
|||
controllerAs: 'vm', |
|||
templateUrl: navTreeTemplate |
|||
}; |
|||
} |
|||
|
|||
/*@ngInject*/ |
|||
function NavTreeController($scope, $element, types) { |
|||
|
|||
var vm = this; |
|||
vm.types = types; |
|||
|
|||
$scope.$watch('vm.loadNodes', (newVal) => { |
|||
if (newVal) { |
|||
initTree(); |
|||
} |
|||
}); |
|||
|
|||
function initTree() { |
|||
vm.treeElement = angular.element('.tb-nav-tree-container', $element) |
|||
.jstree( |
|||
{ |
|||
core: { |
|||
multiple: false, |
|||
check_callback: true, |
|||
themes: { name: 'proton', responsive: true }, |
|||
data: vm.loadNodes |
|||
} |
|||
} |
|||
); |
|||
|
|||
vm.treeElement.on("changed.jstree", function (e, data) { |
|||
if (vm.onNodeSelected) { |
|||
vm.onNodeSelected({node: data.instance.get_selected(true)[0], event: e}); |
|||
} |
|||
}); |
|||
|
|||
vm.treeElement.on("model.jstree", function (e, data) { |
|||
if (vm.onNodesInserted) { |
|||
vm.onNodesInserted({nodes: data.nodes, parent: data.parent}); |
|||
} |
|||
}); |
|||
|
|||
if (vm.editCallbacks) { |
|||
vm.editCallbacks.selectNode = (id) => { |
|||
var node = vm.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
vm.treeElement.jstree('deselect_all', true); |
|||
vm.treeElement.jstree('select_node', node); |
|||
} |
|||
}; |
|||
vm.editCallbacks.deselectAll = () => { |
|||
vm.treeElement.jstree('deselect_all'); |
|||
}; |
|||
vm.editCallbacks.getNode = (id) => { |
|||
var node = vm.treeElement.jstree('get_node', id); |
|||
return node; |
|||
}; |
|||
vm.editCallbacks.getParentNodeId = (id) => { |
|||
var node = vm.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
return vm.treeElement.jstree('get_parent', node); |
|||
} |
|||
}; |
|||
vm.editCallbacks.openNode = (id, cb) => { |
|||
var node = vm.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
vm.treeElement.jstree('open_node', node, cb); |
|||
} |
|||
}; |
|||
vm.editCallbacks.nodeIsOpen = (id) => { |
|||
var node = vm.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
return vm.treeElement.jstree('is_open', node); |
|||
} else { |
|||
return true; |
|||
} |
|||
}; |
|||
vm.editCallbacks.nodeIsLoaded = (id) => { |
|||
var node = vm.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
return vm.treeElement.jstree('is_loaded', node); |
|||
} else { |
|||
return true; |
|||
} |
|||
}; |
|||
vm.editCallbacks.refreshNode = (id) => { |
|||
if (id === '#') { |
|||
vm.treeElement.jstree('refresh'); |
|||
vm.treeElement.jstree('redraw'); |
|||
} else { |
|||
var node = vm.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
var opened = vm.treeElement.jstree('is_open', node); |
|||
vm.treeElement.jstree('refresh_node', node); |
|||
vm.treeElement.jstree('redraw'); |
|||
if (node.children && opened/* && !node.children.length*/) { |
|||
vm.treeElement.jstree('open_node', node); |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
vm.editCallbacks.updateNode = (id, newName) => { |
|||
var node = vm.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
vm.treeElement.jstree('rename_node', node, newName); |
|||
} |
|||
}; |
|||
vm.editCallbacks.createNode = (parentId, node, pos) => { |
|||
var parentNode = vm.treeElement.jstree('get_node', parentId); |
|||
if (parentNode) { |
|||
vm.treeElement.jstree('create_node', parentNode, node, pos); |
|||
} |
|||
}; |
|||
vm.editCallbacks.deleteNode = (id) => { |
|||
var node = vm.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
vm.treeElement.jstree('delete_node', node); |
|||
} |
|||
}; |
|||
vm.editCallbacks.disableNode = (id) => { |
|||
var node = vm.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
vm.treeElement.jstree('disable_node', node); |
|||
} |
|||
}; |
|||
vm.editCallbacks.enableNode = (id) => { |
|||
var node = vm.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
vm.treeElement.jstree('enable_node', node); |
|||
} |
|||
}; |
|||
vm.editCallbacks.setNodeHasChildren = (id, hasChildren) => { |
|||
var node = vm.treeElement.jstree('get_node', id); |
|||
if (node) { |
|||
if (!node.children || !node.children.length) { |
|||
node.children = hasChildren; |
|||
node.state.loaded = !hasChildren; |
|||
node.state.opened = false; |
|||
vm.treeElement.jstree('_node_changed', node.id); |
|||
vm.treeElement.jstree('redraw'); |
|||
} |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,346 @@ |
|||
/** |
|||
* 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("../../png/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("../../png/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("../../png/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("../../png/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 .md-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 { |
|||
.md-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,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,469 @@ |
|||
/* |
|||
* 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 './entities-hierarchy-widget.scss'; |
|||
|
|||
/* eslint-disable import/no-unresolved, import/default */ |
|||
|
|||
import entitiesHierarchyWidgetTemplate from './entities-hierarchy-widget.tpl.html'; |
|||
|
|||
/* eslint-enable import/no-unresolved, import/default */ |
|||
|
|||
export default angular.module('thingsboard.widgets.entitiesHierarchyWidget', []) |
|||
.directive('tbEntitiesHierarchyWidget', EntitiesHierarchyWidget) |
|||
.name; |
|||
|
|||
/*@ngInject*/ |
|||
function EntitiesHierarchyWidget() { |
|||
return { |
|||
restrict: "E", |
|||
scope: true, |
|||
bindToController: { |
|||
hierarchyId: '=', |
|||
ctx: '=' |
|||
}, |
|||
controller: EntitiesHierarchyWidgetController, |
|||
controllerAs: 'vm', |
|||
templateUrl: entitiesHierarchyWidgetTemplate |
|||
}; |
|||
} |
|||
|
|||
/*@ngInject*/ |
|||
function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast, types, entityService, entityRelationService /*$filter, $mdMedia, $mdPanel, $document, $translate, $timeout, utils, types*/) { |
|||
var vm = this; |
|||
|
|||
vm.showData = true; |
|||
|
|||
vm.nodeEditCallbacks = {}; |
|||
|
|||
vm.nodeIdCounter = 0; |
|||
|
|||
vm.nodesMap = {}; |
|||
vm.pendingUpdateNodeTasks = {}; |
|||
|
|||
$scope.$watch('vm.ctx', function() { |
|||
if (vm.ctx && vm.ctx.defaultSubscription) { |
|||
vm.settings = vm.ctx.settings; |
|||
vm.widgetConfig = vm.ctx.widgetConfig; |
|||
vm.subscription = vm.ctx.defaultSubscription; |
|||
vm.datasources = vm.subscription.datasources; |
|||
initializeConfig(); |
|||
updateDatasources(); |
|||
} |
|||
}); |
|||
|
|||
$scope.$on('entities-hierarchy-data-updated', function(event, hierarchyId) { |
|||
if (vm.hierarchyId == hierarchyId) { |
|||
if (vm.subscription) { |
|||
updateNodeData(vm.subscription.data); |
|||
} |
|||
} |
|||
}); |
|||
|
|||
vm.onNodesInserted = onNodesInserted; |
|||
|
|||
vm.onNodeSelected = onNodeSelected; |
|||
|
|||
function initializeConfig() { |
|||
|
|||
var testNodeCtx = { |
|||
entity: { |
|||
id: { |
|||
entityType: 'DEVICE', |
|||
id: '123' |
|||
}, |
|||
name: 'TEST DEV1' |
|||
}, |
|||
data: {}, |
|||
level: 2 |
|||
}; |
|||
var parentNodeCtx = angular.copy(testNodeCtx); |
|||
parentNodeCtx.level = 1; |
|||
testNodeCtx.parentNodeCtx = parentNodeCtx; |
|||
|
|||
var nodeRelationQueryFunction = loadNodeCtxFunction(vm.settings.nodeRelationQueryFunction, 'nodeCtx', testNodeCtx); |
|||
var nodeIconFunction = loadNodeCtxFunction(vm.settings.nodeIconFunction, 'nodeCtx', testNodeCtx); |
|||
var nodeTextFunction = loadNodeCtxFunction(vm.settings.nodeTextFunction, 'nodeCtx', testNodeCtx); |
|||
var nodeDisabledFunction = loadNodeCtxFunction(vm.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx); |
|||
var nodeHasChildrenFunction = loadNodeCtxFunction(vm.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx); |
|||
|
|||
var testNodeCtx2 = angular.copy(testNodeCtx); |
|||
testNodeCtx2.entity.name = 'TEST DEV2'; |
|||
|
|||
var nodesSortFunction = loadNodeCtxFunction(vm.settings.nodesSortFunction, 'nodeCtx1,nodeCtx2', testNodeCtx, testNodeCtx2); |
|||
|
|||
vm.nodeRelationQueryFunction = nodeRelationQueryFunction || defaultNodeRelationQueryFunction; |
|||
vm.nodeIconFunction = nodeIconFunction || defaultNodeIconFunction; |
|||
vm.nodeTextFunction = nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name); |
|||
vm.nodeDisabledFunction = nodeDisabledFunction || (() => false); |
|||
vm.nodeHasChildrenFunction = nodeHasChildrenFunction || (() => true); |
|||
vm.nodesSortFunction = nodesSortFunction || defaultSortFunction; |
|||
} |
|||
|
|||
function loadNodeCtxFunction(functionBody, argNames, ...args) { |
|||
var nodeCtxFunction = null; |
|||
if (angular.isDefined(functionBody) && functionBody.length) { |
|||
try { |
|||
nodeCtxFunction = new Function(argNames, functionBody); |
|||
var res = nodeCtxFunction.apply(null, args); |
|||
if (angular.isUndefined(res)) { |
|||
nodeCtxFunction = null; |
|||
} |
|||
} catch (e) { |
|||
nodeCtxFunction = null; |
|||
} |
|||
} |
|||
return nodeCtxFunction; |
|||
} |
|||
|
|||
function updateDatasources() { |
|||
vm.loadNodes = loadNodes; |
|||
} |
|||
|
|||
function onNodesInserted(nodes/*, parent*/) { |
|||
if (nodes) { |
|||
nodes.forEach((nodeId) => { |
|||
var task = vm.pendingUpdateNodeTasks[nodeId]; |
|||
if (task) { |
|||
task(); |
|||
delete vm.pendingUpdateNodeTasks[nodeId]; |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
function onNodeSelected(node, event) { |
|||
var nodeId; |
|||
if (!node) { |
|||
nodeId = -1; |
|||
} else { |
|||
nodeId = node.id; |
|||
} |
|||
if (nodeId !== -1) { |
|||
var selectedNode = vm.nodesMap[nodeId]; |
|||
if (selectedNode) { |
|||
var descriptors = vm.ctx.actionsApi.getActionDescriptors('nodeSelected'); |
|||
if (descriptors.length) { |
|||
var entity = selectedNode.data.nodeCtx.entity; |
|||
vm.ctx.actionsApi.handleWidgetAction(event, descriptors[0], entity.id, entity.name, { nodeCtx: selectedNode.data.nodeCtx }); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
function updateNodeData(subscriptionData) { |
|||
var affectedNodes = []; |
|||
if (subscriptionData) { |
|||
for (var i=0;i<subscriptionData.length;i++) { |
|||
var datasource = subscriptionData[i].datasource; |
|||
if (datasource.nodeId) { |
|||
var node = vm.nodesMap[datasource.nodeId]; |
|||
var key = subscriptionData[i].dataKey.label; |
|||
var value = undefined; |
|||
if (subscriptionData[i].data && subscriptionData[i].data.length) { |
|||
value = subscriptionData[i].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) => { |
|||
var node = vm.nodeEditCallbacks.getNode(nodeId); |
|||
if (node) { |
|||
updateNodeStyle(vm.nodesMap[nodeId]); |
|||
} else { |
|||
vm.pendingUpdateNodeTasks[nodeId] = () => { |
|||
updateNodeStyle(vm.nodesMap[nodeId]); |
|||
}; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
function updateNodeStyle(node) { |
|||
var newText = prepareNodeText(node); |
|||
if (!angular.equals(node.text, newText)) { |
|||
node.text = newText; |
|||
vm.nodeEditCallbacks.updateNode(node.id, node.text); |
|||
} |
|||
var newDisabled = vm.nodeDisabledFunction(node.data.nodeCtx); |
|||
if (!angular.equals(node.state.disabled, newDisabled)) { |
|||
node.state.disabled = newDisabled; |
|||
if (node.state.disabled) { |
|||
vm.nodeEditCallbacks.disableNode(node.id); |
|||
} else { |
|||
vm.nodeEditCallbacks.enableNode(node.id); |
|||
} |
|||
} |
|||
var newHasChildren = vm.nodeHasChildrenFunction(node.data.nodeCtx); |
|||
if (!angular.equals(node.children, newHasChildren)) { |
|||
node.children = newHasChildren; |
|||
vm.nodeEditCallbacks.setNodeHasChildren(node.id, node.children); |
|||
} |
|||
} |
|||
|
|||
function prepareNodeText(node) { |
|||
var nodeIcon = prepareNodeIcon(node.data.nodeCtx); |
|||
var nodeText = vm.nodeTextFunction(node.data.nodeCtx); |
|||
return nodeIcon + nodeText; |
|||
} |
|||
|
|||
function loadNodes(node, cb) { |
|||
if (node.id === '#') { |
|||
var tasks = []; |
|||
for (var i=0;i<vm.datasources.length;i++) { |
|||
var datasource = vm.datasources[i]; |
|||
tasks.push(datasourceToNode(datasource)); |
|||
} |
|||
$q.all(tasks).then((nodes) => { |
|||
cb(prepareNodes(nodes)); |
|||
updateNodeData(vm.subscription.data); |
|||
}); |
|||
} else { |
|||
if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.nodeCtx.entity.id.entityType !== 'function') { |
|||
var relationQuery = prepareNodeRelationQuery(node.data.nodeCtx); |
|||
entityRelationService.findByQuery(relationQuery, {ignoreErrors: true, ignoreLoading: true}).then( |
|||
(entityRelations) => { |
|||
var tasks = []; |
|||
for (var i=0;i<entityRelations.length;i++) { |
|||
var relation = entityRelations[i]; |
|||
var targetId = relationQuery.parameters.direction === types.entitySearchDirection.from ? relation.to : relation.from; |
|||
tasks.push(entityIdToNode(targetId.entityType, targetId.id, node.data.datasource, node.data.nodeCtx)); |
|||
} |
|||
$q.all(tasks).then((nodes) => { |
|||
cb(prepareNodes(nodes)); |
|||
}); |
|||
}, |
|||
(error) => { |
|||
var errorText = "Failed to get relations!"; |
|||
if (error && error.status === 400) { |
|||
errorText = "Invalid relations query returned by 'Node relations query function'! Please check widget configuration!"; |
|||
} |
|||
showError(errorText); |
|||
} |
|||
); |
|||
} else { |
|||
cb([]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
function showError(errorText) { |
|||
var toastParent = angular.element('.tb-entities-hierarchy', $element); |
|||
toast.showError(errorText, toastParent, 'bottom left'); |
|||
} |
|||
|
|||
function prepareNodes(nodes) { |
|||
nodes = nodes.filter((node) => node !== null); |
|||
nodes.sort((node1, node2) => vm.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx)); |
|||
return nodes; |
|||
} |
|||
|
|||
function datasourceToNode(datasource, parentNodeCtx) { |
|||
var deferred = $q.defer(); |
|||
resolveEntity(datasource).then( |
|||
(entity) => { |
|||
if (entity != null) { |
|||
var node = { |
|||
id: ++vm.nodeIdCounter |
|||
}; |
|||
vm.nodesMap[node.id] = node; |
|||
datasource.nodeId = node.id; |
|||
node.icon = false; |
|||
var nodeCtx = { |
|||
parentNodeCtx: parentNodeCtx, |
|||
entity: entity, |
|||
data: {} |
|||
}; |
|||
nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1; |
|||
node.data = { |
|||
datasource: datasource, |
|||
nodeCtx: nodeCtx |
|||
}; |
|||
node.state = { |
|||
disabled: vm.nodeDisabledFunction(node.data.nodeCtx) |
|||
}; |
|||
node.text = prepareNodeText(node); |
|||
node.children = vm.nodeHasChildrenFunction(node.data.nodeCtx); |
|||
deferred.resolve(node); |
|||
} else { |
|||
deferred.resolve(null); |
|||
} |
|||
} |
|||
); |
|||
return deferred.promise; |
|||
} |
|||
|
|||
function entityIdToNode(entityType, entityId, parentDatasource, parentNodeCtx) { |
|||
var deferred = $q.defer(); |
|||
var datasource = { |
|||
dataKeys: parentDatasource.dataKeys, |
|||
type: types.datasourceType.entity, |
|||
entityType: entityType, |
|||
entityId: entityId |
|||
}; |
|||
datasourceToNode(datasource, parentNodeCtx).then( |
|||
(node) => { |
|||
if (node != null) { |
|||
var subscriptionOptions = { |
|||
type: types.widgetType.latest.value, |
|||
datasources: [datasource], |
|||
callbacks: { |
|||
onDataUpdated: (subscription) => { |
|||
updateNodeData(subscription.data); |
|||
} |
|||
} |
|||
}; |
|||
vm.ctx.subscriptionApi.createSubscription(subscriptionOptions, true).then( |
|||
(/*subscription*/) => { |
|||
deferred.resolve(node); |
|||
} |
|||
); |
|||
} else { |
|||
deferred.resolve(node); |
|||
} |
|||
} |
|||
); |
|||
return deferred.promise; |
|||
} |
|||
|
|||
function resolveEntity(datasource) { |
|||
var deferred = $q.defer(); |
|||
if (datasource.type === types.datasourceType.function) { |
|||
var entity = { |
|||
id: { |
|||
entityType: "function" |
|||
}, |
|||
name: datasource.name |
|||
} |
|||
deferred.resolve(entity); |
|||
} else { |
|||
entityService.getEntity(datasource.entityType, datasource.entityId, {ignoreLoading: true}).then( |
|||
(entity) => { |
|||
deferred.resolve(entity); |
|||
}, |
|||
() => { |
|||
deferred.resolve(null); |
|||
} |
|||
); |
|||
} |
|||
return deferred.promise; |
|||
} |
|||
|
|||
|
|||
function prepareNodeRelationQuery(nodeCtx) { |
|||
var relationQuery = vm.nodeRelationQueryFunction(nodeCtx); |
|||
if (relationQuery && relationQuery === 'default') { |
|||
relationQuery = defaultNodeRelationQueryFunction(nodeCtx); |
|||
} |
|||
return relationQuery; |
|||
} |
|||
|
|||
function defaultNodeRelationQueryFunction(nodeCtx) { |
|||
var entity = nodeCtx.entity; |
|||
var query = { |
|||
parameters: { |
|||
rootId: entity.id.id, |
|||
rootType: entity.id.entityType, |
|||
direction: types.entitySearchDirection.from, |
|||
relationTypeGroup: "COMMON", |
|||
maxLevel: 1 |
|||
}, |
|||
filters: [ |
|||
{ |
|||
relationType: "Contains", |
|||
entityTypes: [] |
|||
} |
|||
] |
|||
}; |
|||
return query; |
|||
} |
|||
|
|||
function prepareNodeIcon(nodeCtx) { |
|||
var iconInfo = vm.nodeIconFunction(nodeCtx); |
|||
if (iconInfo && iconInfo === 'default') { |
|||
iconInfo = defaultNodeIconFunction(nodeCtx); |
|||
} |
|||
if (iconInfo && (iconInfo.iconUrl || iconInfo.materialIcon)) { |
|||
if (iconInfo.materialIcon) { |
|||
return materialIconHtml(iconInfo.materialIcon); |
|||
} else { |
|||
return iconUrlHtml(iconInfo.iconUrl); |
|||
} |
|||
} else { |
|||
return ""; |
|||
} |
|||
} |
|||
|
|||
function materialIconHtml(materialIcon) { |
|||
return '<md-icon aria-label="'+materialIcon+'" class="node-icon material-icons" role="img" aria-hidden="false">'+materialIcon+'</md-icon>'; |
|||
} |
|||
|
|||
function iconUrlHtml(iconUrl) { |
|||
return '<div class="node-icon" style="background-image: url('+iconUrl+');"> </div>'; |
|||
} |
|||
|
|||
function defaultNodeIconFunction(nodeCtx) { |
|||
var materialIcon = 'insert_drive_file'; |
|||
var entity = nodeCtx.entity; |
|||
if (entity && entity.id && entity.id.entityType) { |
|||
switch (entity.id.entityType) { |
|||
case 'function': |
|||
materialIcon = 'functions'; |
|||
break; |
|||
case types.entityType.device: |
|||
materialIcon = 'devices_other'; |
|||
break; |
|||
case types.entityType.asset: |
|||
materialIcon = 'domain'; |
|||
break; |
|||
case types.entityType.tenant: |
|||
materialIcon = 'supervisor_account'; |
|||
break; |
|||
case types.entityType.customer: |
|||
materialIcon = 'supervisor_account'; |
|||
break; |
|||
case types.entityType.user: |
|||
materialIcon = 'account_circle'; |
|||
break; |
|||
case types.entityType.dashboard: |
|||
materialIcon = 'dashboards'; |
|||
break; |
|||
case types.entityType.alarm: |
|||
materialIcon = 'notifications_active'; |
|||
break; |
|||
case types.entityType.entityView: |
|||
materialIcon = 'view_quilt'; |
|||
break; |
|||
} |
|||
} |
|||
return { |
|||
materialIcon: materialIcon |
|||
}; |
|||
} |
|||
|
|||
function defaultSortFunction(nodeCtx1, nodeCtx2) { |
|||
var 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,96 @@ |
|||
/** |
|||
* 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-entities-hierarchy { |
|||
.tb-entities-nav-tree-panel { |
|||
overflow-x: auto; |
|||
overflow-y: auto; |
|||
|
|||
.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; |
|||
} |
|||
|
|||
md-icon.node-icon { |
|||
width: 22px; |
|||
min-width: 22px; |
|||
height: 22px; |
|||
min-height: 22px; |
|||
margin-right: 2px; |
|||
margin-bottom: 2px; |
|||
color: inherit; |
|||
|
|||
&.material-icons { /* stylelint-disable-line selector-max-class */ |
|||
font-size: 18px; |
|||
line-height: 22px; |
|||
text-align: center; |
|||
} |
|||
} |
|||
|
|||
&.jstree-hovered:not(.jstree-clicked), |
|||
&.jstree-disabled { |
|||
div.node-icon { /* stylelint-disable-line selector-max-class */ |
|||
opacity: .5; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 768px) { |
|||
.tb-entities-hierarchy { |
|||
.tb-entities-nav-tree-panel { |
|||
.tb-nav-tree-container { |
|||
&.jstree-proton-responsive { |
|||
.jstree-anchor { |
|||
div.node-icon { |
|||
width: 40px; |
|||
height: 40px; |
|||
margin: 0; |
|||
background-size: 24px 24px; |
|||
} |
|||
|
|||
md-icon.node-icon { |
|||
width: 40px; |
|||
min-width: 40px; |
|||
height: 40px; |
|||
min-height: 40px; |
|||
margin: 0; |
|||
|
|||
&.material-icons { /* stylelint-disable-line selector-max-class */ |
|||
font-size: 24px; |
|||
line-height: 40px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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. |
|||
|
|||
--> |
|||
<div class="tb-absolute-fill tb-entities-hierarchy" layout="column"> |
|||
<div ng-show="vm.showData" flex class="tb-absolute-fill" layout="column"> |
|||
<div flex class="tb-entities-nav-tree-panel"> |
|||
<tb-nav-tree |
|||
load-nodes="vm.loadNodes" |
|||
on-node-selected="vm.onNodeSelected(node, event)" |
|||
on-nodes-inserted="vm.onNodesInserted(nodes, parent)" |
|||
edit-callbacks="vm.nodeEditCallbacks" |
|||
></tb-nav-tree> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
Loading…
Reference in new issue