diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json
index 6550f58da4..8833517a7b 100644
--- a/application/src/main/data/json/system/widget_bundles/cards.json
+++ b/application/src/main/data/json/system/widget_bundles/cards.json
@@ -116,6 +116,22 @@
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}"
}
+ },
+ {
+ "alias": "entities_hierarchy",
+ "name": "Entities hierarchy",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 7.5,
+ "sizeY": 3.5,
+ "resources": [],
+ "templateHtml": "\n",
+ "templateCss": "",
+ "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.hierarchyId = \"hierarchy-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-hierarchy-data-updated', self.ctx.$scope.hierarchyId);\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'nodeSelected': {\n name: 'widget-action.node-selected',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
+ "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesHierarchySettings\",\n \"properties\": {\n \"nodeRelationQueryFunction\": {\n \"title\": \"Node relations query function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeHasChildrenFunction\": {\n \"title\": \"Node has children function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeDisabledFunction\": {\n \"title\": \"Node disabled function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeIconFunction\": {\n \"title\": \"Node icon function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeTextFunction\": {\n \"title\": \"Node text function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodesSortFunction\": {\n \"title\": \"Nodes sort function: f(nodeCtx1, nodeCtx2)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"nodeRelationQueryFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeHasChildrenFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeDisabledFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeIconFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeTextFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodesSortFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
+ "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\n}",
+ "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"nodeRelationQueryFunction\":\"/**\\n\\n// Function should return relations query object for current node used to fetch entity children.\\n// Function can return 'default' string value. In this case default relations query will be used.\\n\\n// The following example code will construct simple relations query that will fetch relations of type 'Contains'\\n// from the current entity.\\n\\nvar entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: types.entitySearchDirection.from,\\n relationTypeGroup: \\\"COMMON\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n**/\\n\",\"nodeHasChildrenFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node has children (whether it can be expanded).\\n\\n// The following example code will restrict entities hierarchy expansion up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n// The next example code will restrict entities expansion according to the value of example 'nodeHasChildren' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeHasChildren') && data['nodeHasChildren'] !== null) {\\n return data['nodeHasChildren'] === 'true';\\n} else {\\n return true;\\n}\\n \\n**/\\n \",\"nodeTextFunction\":\"/**\\n\\n// Function should return text (can be HTML code) for the current node.\\n\\n// The following example code will generate node text consisting of entity name and temperature if temperature value is present in entity attributes/timeseries.\\n\\nvar data = nodeCtx.data;\\nvar entity = nodeCtx.entity;\\nvar text = entity.name;\\nif (data.hasOwnProperty('temperature') && data['temperature'] !== null) {\\n text += \\\" \\\"+ data['temperature'] +\\\" °C\\\";\\n}\\nreturn text;\\n\\n**/\",\"nodeIconFunction\":\"/** \\n\\n// Function should return node icon info object.\\n// Resulting object should contain either 'materialIcon' or 'iconUrl' property. \\n// Where:\\n - 'materialIcon' - name of the material icon to be used from the Material Icons Library (https://material.io/tools/icons);\\n - 'iconUrl' - url of the external image to be used as node icon.\\n// Function can return 'default' string value. In this case default icons according to entity type will be used.\\n\\n// The following example code shows how to use external image for devices which name starts with 'Test' and use \\n// default icons for the rest of entities.\\n\\nvar entity = nodeCtx.entity;\\nif (entity.id.entityType === 'DEVICE' && entity.name.startsWith('Test')) {\\n return {iconUrl: 'https://avatars1.githubusercontent.com/u/14793288?v=4&s=117'};\\n} else {\\n return 'default';\\n}\\n \\n**/\",\"nodeDisabledFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be disabled (not selectable).\\n\\n// The following example code will disable current node according to the value of example 'nodeDisabled' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeDisabled') && data['nodeDisabled'] !== null) {\\n return data['nodeDisabled'] === 'true';\\n} else {\\n return false;\\n}\\n \\n**/\\n\",\"nodesSortFunction\":\"/**\\n\\n// This function is used to sort nodes of the same level. Function should compare two nodes and return \\n// integer value: \\n// - less than 0 - sort nodeCtx1 to an index lower than nodeCtx2\\n// - 0 - leave nodeCtx1 and nodeCtx2 unchanged with respect to each other\\n// - greater than 0 - sort nodeCtx2 to an index lower than nodeCtx1\\n\\n// The following example code will sort entities first by entity type in alphabetical order then\\n// by entity name in alphabetical order.\\n\\nvar result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);\\nif (result === 0) {\\n result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);\\n}\\nreturn result;\\n \\n**/\"},\"title\":\"Entities hierarchy\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"widgetStyle\":{},\"actions\":{}}"
+ }
}
]
}
\ No newline at end of file
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 8275da082b..39846f4c4e 100644
--- a/ui/package-lock.json
+++ b/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",
diff --git a/ui/package.json b/ui/package.json
index 89ee741881..0a1f4512a1 100644
--- a/ui/package.json
+++ b/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",
diff --git a/ui/src/app/api/entity-relation.service.js b/ui/src/app/api/entity-relation.service.js
index ad5dc63da4..9294ca39d4 100644
--- a/ui/src/app/api/entity-relation.service.js
+++ b/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;
}
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index 92b9275ce4..70cf76ff1a 100644
--- a/ui/src/app/api/widget.service.js
+++ b/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;
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index dbef8a9c6b..7081415ff5 100644
--- a/ui/src/app/app.js
+++ b/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';
diff --git a/ui/src/app/components/nav-tree.directive.js b/ui/src/app/components/nav-tree.directive.js
new file mode 100644
index 0000000000..c179defa8e
--- /dev/null
+++ b/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');
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/ui/src/app/components/nav-tree.scss b/ui/src/app/components/nav-tree.scss
new file mode 100644
index 0000000000..4a9ac4739b
--- /dev/null
+++ b/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;
+ }
+ }
+ }
+}
diff --git a/ui/src/app/components/nav-tree.tpl.html b/ui/src/app/components/nav-tree.tpl.html
new file mode 100644
index 0000000000..88612965ff
--- /dev/null
+++ b/ui/src/app/components/nav-tree.tpl.html
@@ -0,0 +1,18 @@
+
+
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index bb450490a8..c0ef817a3b 100644
--- a/ui/src/app/layout/index.js
+++ b/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,
diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json
index 9e38c55aaa..dfef502f09 100644
--- a/ui/src/app/locale/locale.constant-en_US.json
+++ b/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": {
diff --git a/ui/src/app/widget/lib/entities-hierarchy-widget.js b/ui/src/app/widget/lib/entities-hierarchy-widget.js
new file mode 100644
index 0000000000..80a92d68f6
--- /dev/null
+++ b/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 {
+ 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 {
+ 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 {
+ 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 ''+materialIcon+'';
+ }
+
+ function iconUrlHtml(iconUrl) {
+ return '
';
+ }
+
+ 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;
+ }
+}
diff --git a/ui/src/app/widget/lib/entities-hierarchy-widget.scss b/ui/src/app/widget/lib/entities-hierarchy-widget.scss
new file mode 100644
index 0000000000..06ac2fc73a
--- /dev/null
+++ b/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;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html b/ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html
new file mode 100644
index 0000000000..a3ea257858
--- /dev/null
+++ b/ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html
@@ -0,0 +1,29 @@
+
+
diff --git a/ui/src/png/jstree/32px.png b/ui/src/png/jstree/32px.png
new file mode 100644
index 0000000000..719a6bcdbd
Binary files /dev/null and b/ui/src/png/jstree/32px.png differ
diff --git a/ui/src/png/jstree/40px.png b/ui/src/png/jstree/40px.png
new file mode 100644
index 0000000000..1959347aea
Binary files /dev/null and b/ui/src/png/jstree/40px.png differ