Browse Source

Entities hierarchy widget.

pull/1558/head
Igor Kulikov 7 years ago
parent
commit
0d933cf065
  1. 16
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 16
      ui/package-lock.json
  3. 2
      ui/package.json
  4. 8
      ui/src/app/api/entity-relation.service.js
  5. 3
      ui/src/app/api/widget.service.js
  6. 3
      ui/src/app/app.js
  7. 185
      ui/src/app/components/nav-tree.directive.js
  8. 346
      ui/src/app/components/nav-tree.scss
  9. 18
      ui/src/app/components/nav-tree.tpl.html
  10. 2
      ui/src/app/layout/index.js
  11. 3
      ui/src/app/locale/locale.constant-en_US.json
  12. 469
      ui/src/app/widget/lib/entities-hierarchy-widget.js
  13. 96
      ui/src/app/widget/lib/entities-hierarchy-widget.scss
  14. 29
      ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html
  15. BIN
      ui/src/png/jstree/32px.png
  16. BIN
      ui/src/png/jstree/40px.png

16
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

16
ui/package-lock.json

@ -7675,6 +7675,22 @@
}
}
},
"jstree": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/jstree/-/jstree-3.3.7.tgz",
"integrity": "sha512-yzzalO1TbZ4HdPezO43LesGI4Wv2sB0Nl+4GfwO0YYvehGws5qtTAhlBISxfur9phMLwCtf9GjHlRx2ZLXyRnw==",
"requires": {
"jquery": ">=1.9.1"
}
},
"jstree-bootstrap-theme": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/jstree-bootstrap-theme/-/jstree-bootstrap-theme-1.0.1.tgz",
"integrity": "sha1-fV7cc6hG6Np/lPV6HMXd7p2eq0s=",
"requires": {
"jquery": ">=1.9.1"
}
},
"keycode": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz",

2
ui/package.json

@ -60,6 +60,8 @@
"jquery.terminal": "^1.5.0",
"js-beautify": "^1.6.4",
"json-schema-defaults": "^0.2.0",
"jstree": "^3.3.7",
"jstree-bootstrap-theme": "^1.0.1",
"leaflet": "^1.0.3",
"leaflet-providers": "^1.1.17",
"material-ui": "^0.16.1",

8
ui/src/app/api/entity-relation.service.js

@ -164,13 +164,13 @@ function EntityRelationService($http, $q) {
return deferred.promise;
}
function findByQuery(query) {
function findByQuery(query, config) {
var deferred = $q.defer();
var url = '/api/relations';
$http.post(url, query).then(function success(response) {
$http.post(url, query, config).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
}, function fail(e) {
deferred.reject(e);
});
return deferred.promise;
}

3
ui/src/app/api/widget.service.js

@ -21,6 +21,7 @@ import thingsboardLedLight from '../components/led-light.directive';
import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget';
import thingsboardEntitiesHierarchyWidget from '../widget/lib/entities-hierarchy-widget';
import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
import thingsboardRpcWidgets from '../widget/lib/rpc';
@ -44,7 +45,7 @@ import thingsboardTypes from '../common/types.constant';
import thingsboardUtils from '../common/utils.service';
export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget])
thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget])
.factory('widgetService', WidgetService)
.name;

3
ui/src/app/app.js

@ -52,7 +52,8 @@ import 'react-schema-form';
import react from 'ngreact';
import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
import 'ngFlowchart/dist/ngFlowchart';
import 'jstree/dist/jstree.min';
import 'jstree-bootstrap-theme/dist/themes/proton/style.min.css';
import 'typeface-roboto';
import 'font-awesome/css/font-awesome.min.css';
import 'angular-material/angular-material.min.css';

185
ui/src/app/components/nav-tree.directive.js

@ -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');
}
}
};
}
}
}

346
ui/src/app/components/nav-tree.scss

@ -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;
}
}
}
}

18
ui/src/app/components/nav-tree.tpl.html

@ -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>

2
ui/src/app/layout/index.js

@ -27,6 +27,7 @@ import thingsboardApiUser from '../api/user.service';
import thingsboardNoAnimate from '../components/no-animate.directive';
import thingsboardOnFinishRender from '../components/finish-render.directive';
import thingsboardSideMenu from '../components/side-menu.directive';
import thingsboardNavTree from '../components/nav-tree.directive';
import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive';
import thingsboardKvMap from '../components/kv-map.directive';
import thingsboardJsonObjectEdit from '../components/json-object-edit.directive';
@ -89,6 +90,7 @@ export default angular.module('thingsboard.home', [
thingsboardNoAnimate,
thingsboardOnFinishRender,
thingsboardSideMenu,
thingsboardNavTree,
thingsboardDashboardAutocomplete,
thingsboardKvMap,
thingsboardJsonObjectEdit,

3
ui/src/app/locale/locale.constant-en_US.json

@ -1566,7 +1566,8 @@
"row-click": "On row click",
"polygon-click": "On polygon click",
"marker-click": "On marker click",
"tooltip-tag-action": "Tooltip tag action"
"tooltip-tag-action": "Tooltip tag action",
"node-selected": "On node selected"
}
},
"language": {

469
ui/src/app/widget/lib/entities-hierarchy-widget.js

@ -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+');">&nbsp;</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;
}
}

96
ui/src/app/widget/lib/entities-hierarchy-widget.scss

@ -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;
}
}
}
}
}
}
}
}

29
ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html

@ -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>

BIN
ui/src/png/jstree/32px.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
ui/src/png/jstree/40px.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Loading…
Cancel
Save