Commit 0d933cf0652befd9887ee9af36d20d4d8e3c7d44

Authored by Igor Kulikov
1 parent effb629a

Entities hierarchy widget.

... ... @@ -116,6 +116,22 @@
116 116 "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}",
117 117 "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\":{}}"
118 118 }
  119 + },
  120 + {
  121 + "alias": "entities_hierarchy",
  122 + "name": "Entities hierarchy",
  123 + "descriptor": {
  124 + "type": "latest",
  125 + "sizeX": 7.5,
  126 + "sizeY": 3.5,
  127 + "resources": [],
  128 + "templateHtml": "<tb-entities-hierarchy-widget \n hierarchy-id=\"hierarchyId\"\n ctx=\"ctx\">\n</tb-entities-hierarchy-widget>",
  129 + "templateCss": "",
  130 + "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",
  131 + "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}",
  132 + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\n}",
  133 + "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 += \\\" <b>\\\"+ data['temperature'] +\\\" °C</b>\\\";\\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\":{}}"
  134 + }
119 135 }
120 136 ]
121 137 }
\ No newline at end of file
... ...
... ... @@ -7675,6 +7675,22 @@
7675 7675 }
7676 7676 }
7677 7677 },
  7678 + "jstree": {
  7679 + "version": "3.3.7",
  7680 + "resolved": "https://registry.npmjs.org/jstree/-/jstree-3.3.7.tgz",
  7681 + "integrity": "sha512-yzzalO1TbZ4HdPezO43LesGI4Wv2sB0Nl+4GfwO0YYvehGws5qtTAhlBISxfur9phMLwCtf9GjHlRx2ZLXyRnw==",
  7682 + "requires": {
  7683 + "jquery": ">=1.9.1"
  7684 + }
  7685 + },
  7686 + "jstree-bootstrap-theme": {
  7687 + "version": "1.0.1",
  7688 + "resolved": "https://registry.npmjs.org/jstree-bootstrap-theme/-/jstree-bootstrap-theme-1.0.1.tgz",
  7689 + "integrity": "sha1-fV7cc6hG6Np/lPV6HMXd7p2eq0s=",
  7690 + "requires": {
  7691 + "jquery": ">=1.9.1"
  7692 + }
  7693 + },
7678 7694 "keycode": {
7679 7695 "version": "2.2.0",
7680 7696 "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz",
... ...
... ... @@ -60,6 +60,8 @@
60 60 "jquery.terminal": "^1.5.0",
61 61 "js-beautify": "^1.6.4",
62 62 "json-schema-defaults": "^0.2.0",
  63 + "jstree": "^3.3.7",
  64 + "jstree-bootstrap-theme": "^1.0.1",
63 65 "leaflet": "^1.0.3",
64 66 "leaflet-providers": "^1.1.17",
65 67 "material-ui": "^0.16.1",
... ...
... ... @@ -164,13 +164,13 @@ function EntityRelationService($http, $q) {
164 164 return deferred.promise;
165 165 }
166 166
167   - function findByQuery(query) {
  167 + function findByQuery(query, config) {
168 168 var deferred = $q.defer();
169 169 var url = '/api/relations';
170   - $http.post(url, query).then(function success(response) {
  170 + $http.post(url, query, config).then(function success(response) {
171 171 deferred.resolve(response.data);
172   - }, function fail() {
173   - deferred.reject();
  172 + }, function fail(e) {
  173 + deferred.reject(e);
174 174 });
175 175 return deferred.promise;
176 176 }
... ...
... ... @@ -21,6 +21,7 @@ import thingsboardLedLight from '../components/led-light.directive';
21 21 import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
22 22 import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
23 23 import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget';
  24 +import thingsboardEntitiesHierarchyWidget from '../widget/lib/entities-hierarchy-widget';
24 25 import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
25 26
26 27 import thingsboardRpcWidgets from '../widget/lib/rpc';
... ... @@ -44,7 +45,7 @@ import thingsboardTypes from '../common/types.constant';
44 45 import thingsboardUtils from '../common/utils.service';
45 46
46 47 export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
47   - thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget])
  48 + thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget])
48 49 .factory('widgetService', WidgetService)
49 50 .name;
50 51
... ...
... ... @@ -52,7 +52,8 @@ import 'react-schema-form';
52 52 import react from 'ngreact';
53 53 import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
54 54 import 'ngFlowchart/dist/ngFlowchart';
55   -
  55 +import 'jstree/dist/jstree.min';
  56 +import 'jstree-bootstrap-theme/dist/themes/proton/style.min.css';
56 57 import 'typeface-roboto';
57 58 import 'font-awesome/css/font-awesome.min.css';
58 59 import 'angular-material/angular-material.min.css';
... ...
  1 +/*
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +import './nav-tree.scss';
  17 +
  18 +/* eslint-disable import/no-unresolved, import/default */
  19 +
  20 +import navTreeTemplate from './nav-tree.tpl.html';
  21 +
  22 +/* eslint-enable import/no-unresolved, import/default */
  23 +
  24 +export default angular.module('thingsboard.directives.navTree', [])
  25 + .directive('tbNavTree', NavTree)
  26 + .name;
  27 +
  28 +/*@ngInject*/
  29 +function NavTree() {
  30 + return {
  31 + restrict: "E",
  32 + scope: true,
  33 + bindToController: {
  34 + loadNodes: '=',
  35 + editCallbacks: '=',
  36 + onNodeSelected: '&',
  37 + onNodesInserted: '&'
  38 + },
  39 + controller: NavTreeController,
  40 + controllerAs: 'vm',
  41 + templateUrl: navTreeTemplate
  42 + };
  43 +}
  44 +
  45 +/*@ngInject*/
  46 +function NavTreeController($scope, $element, types) {
  47 +
  48 + var vm = this;
  49 + vm.types = types;
  50 +
  51 + $scope.$watch('vm.loadNodes', (newVal) => {
  52 + if (newVal) {
  53 + initTree();
  54 + }
  55 + });
  56 +
  57 + function initTree() {
  58 + vm.treeElement = angular.element('.tb-nav-tree-container', $element)
  59 + .jstree(
  60 + {
  61 + core: {
  62 + multiple: false,
  63 + check_callback: true,
  64 + themes: { name: 'proton', responsive: true },
  65 + data: vm.loadNodes
  66 + }
  67 + }
  68 + );
  69 +
  70 + vm.treeElement.on("changed.jstree", function (e, data) {
  71 + if (vm.onNodeSelected) {
  72 + vm.onNodeSelected({node: data.instance.get_selected(true)[0], event: e});
  73 + }
  74 + });
  75 +
  76 + vm.treeElement.on("model.jstree", function (e, data) {
  77 + if (vm.onNodesInserted) {
  78 + vm.onNodesInserted({nodes: data.nodes, parent: data.parent});
  79 + }
  80 + });
  81 +
  82 + if (vm.editCallbacks) {
  83 + vm.editCallbacks.selectNode = (id) => {
  84 + var node = vm.treeElement.jstree('get_node', id);
  85 + if (node) {
  86 + vm.treeElement.jstree('deselect_all', true);
  87 + vm.treeElement.jstree('select_node', node);
  88 + }
  89 + };
  90 + vm.editCallbacks.deselectAll = () => {
  91 + vm.treeElement.jstree('deselect_all');
  92 + };
  93 + vm.editCallbacks.getNode = (id) => {
  94 + var node = vm.treeElement.jstree('get_node', id);
  95 + return node;
  96 + };
  97 + vm.editCallbacks.getParentNodeId = (id) => {
  98 + var node = vm.treeElement.jstree('get_node', id);
  99 + if (node) {
  100 + return vm.treeElement.jstree('get_parent', node);
  101 + }
  102 + };
  103 + vm.editCallbacks.openNode = (id, cb) => {
  104 + var node = vm.treeElement.jstree('get_node', id);
  105 + if (node) {
  106 + vm.treeElement.jstree('open_node', node, cb);
  107 + }
  108 + };
  109 + vm.editCallbacks.nodeIsOpen = (id) => {
  110 + var node = vm.treeElement.jstree('get_node', id);
  111 + if (node) {
  112 + return vm.treeElement.jstree('is_open', node);
  113 + } else {
  114 + return true;
  115 + }
  116 + };
  117 + vm.editCallbacks.nodeIsLoaded = (id) => {
  118 + var node = vm.treeElement.jstree('get_node', id);
  119 + if (node) {
  120 + return vm.treeElement.jstree('is_loaded', node);
  121 + } else {
  122 + return true;
  123 + }
  124 + };
  125 + vm.editCallbacks.refreshNode = (id) => {
  126 + if (id === '#') {
  127 + vm.treeElement.jstree('refresh');
  128 + vm.treeElement.jstree('redraw');
  129 + } else {
  130 + var node = vm.treeElement.jstree('get_node', id);
  131 + if (node) {
  132 + var opened = vm.treeElement.jstree('is_open', node);
  133 + vm.treeElement.jstree('refresh_node', node);
  134 + vm.treeElement.jstree('redraw');
  135 + if (node.children && opened/* && !node.children.length*/) {
  136 + vm.treeElement.jstree('open_node', node);
  137 + }
  138 + }
  139 + }
  140 + };
  141 + vm.editCallbacks.updateNode = (id, newName) => {
  142 + var node = vm.treeElement.jstree('get_node', id);
  143 + if (node) {
  144 + vm.treeElement.jstree('rename_node', node, newName);
  145 + }
  146 + };
  147 + vm.editCallbacks.createNode = (parentId, node, pos) => {
  148 + var parentNode = vm.treeElement.jstree('get_node', parentId);
  149 + if (parentNode) {
  150 + vm.treeElement.jstree('create_node', parentNode, node, pos);
  151 + }
  152 + };
  153 + vm.editCallbacks.deleteNode = (id) => {
  154 + var node = vm.treeElement.jstree('get_node', id);
  155 + if (node) {
  156 + vm.treeElement.jstree('delete_node', node);
  157 + }
  158 + };
  159 + vm.editCallbacks.disableNode = (id) => {
  160 + var node = vm.treeElement.jstree('get_node', id);
  161 + if (node) {
  162 + vm.treeElement.jstree('disable_node', node);
  163 + }
  164 + };
  165 + vm.editCallbacks.enableNode = (id) => {
  166 + var node = vm.treeElement.jstree('get_node', id);
  167 + if (node) {
  168 + vm.treeElement.jstree('enable_node', node);
  169 + }
  170 + };
  171 + vm.editCallbacks.setNodeHasChildren = (id, hasChildren) => {
  172 + var node = vm.treeElement.jstree('get_node', id);
  173 + if (node) {
  174 + if (!node.children || !node.children.length) {
  175 + node.children = hasChildren;
  176 + node.state.loaded = !hasChildren;
  177 + node.state.opened = false;
  178 + vm.treeElement.jstree('_node_changed', node.id);
  179 + vm.treeElement.jstree('redraw');
  180 + }
  181 + }
  182 + };
  183 + }
  184 + }
  185 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +.tb-nav-tree-container {
  17 + padding: 15px;
  18 + font-family: Roboto, "Helvetica Neue", sans-serif;
  19 +
  20 + &.jstree-proton {
  21 + .jstree-node,
  22 + .jstree-icon {
  23 + background-image: url("../../png/jstree/32px.png");
  24 + }
  25 +
  26 + .jstree-last {
  27 + background: transparent;
  28 + }
  29 +
  30 + .jstree-themeicon-custom {
  31 + background-image: none;
  32 +
  33 + &.material-icons {
  34 + font-size: 18px;
  35 + }
  36 + }
  37 +
  38 + .jstree-anchor {
  39 + font-size: 16px;
  40 + }
  41 + }
  42 +
  43 + &.jstree-proton-small {
  44 + .jstree-node,
  45 + .jstree-icon {
  46 + background-image: url("../../png/jstree/32px.png");
  47 + }
  48 +
  49 + .jstree-last {
  50 + background: transparent;
  51 + }
  52 +
  53 + .jstree-themeicon-custom {
  54 + background-image: none;
  55 +
  56 + &.material-icons {
  57 + font-size: 14px;
  58 + }
  59 + }
  60 +
  61 + .jstree-anchor {
  62 + font-size: 14px;
  63 + }
  64 + }
  65 +
  66 + &.jstree-proton-large {
  67 + .jstree-node,
  68 + .jstree-icon {
  69 + background-image: url("../../png/jstree/32px.png");
  70 + }
  71 +
  72 + .jstree-last {
  73 + background: transparent;
  74 + }
  75 +
  76 + .jstree-themeicon-custom {
  77 + background-image: none;
  78 +
  79 + &.material-icons {
  80 + font-size: 24px;
  81 + }
  82 + }
  83 +
  84 + .jstree-anchor {
  85 + font-size: 20px;
  86 + }
  87 + }
  88 +
  89 + a {
  90 + border-bottom: none;
  91 +
  92 + i.jstree-themeicon-custom {
  93 + &.tb-user-group {
  94 + &::before {
  95 + content: "account_circle";
  96 + }
  97 + }
  98 +
  99 + &.tb-customer-group {
  100 + &::before {
  101 + content: "supervisor_account";
  102 + }
  103 + }
  104 +
  105 + &.tb-asset-group {
  106 + &::before {
  107 + content: "domain";
  108 + }
  109 + }
  110 +
  111 + &.tb-device-group {
  112 + &::before {
  113 + content: "devices_other";
  114 + }
  115 + }
  116 +
  117 + &.tb-entity-view-group {
  118 + &::before {
  119 + content: "view_quilt";
  120 + }
  121 + }
  122 +
  123 + &.tb-dashboard-group {
  124 + &::before {
  125 + content: "dashboard";
  126 + }
  127 + }
  128 +
  129 + &.tb-customer {
  130 + &::before {
  131 + content: "supervisor_account";
  132 + }
  133 + }
  134 + }
  135 + }
  136 +}
  137 +
  138 +@media (max-width: 768px) {
  139 + .tb-nav-tree-container {
  140 + &.jstree-proton-responsive {
  141 + .jstree-node,
  142 + .jstree-icon,
  143 + .jstree-node > .jstree-ocl,
  144 + .jstree-themeicon,
  145 + .jstree-checkbox {
  146 + background-image: url("../../png/jstree/40px.png");
  147 + background-size: 120px 240px;
  148 + }
  149 +
  150 + .jstree-container-ul {
  151 + overflow: visible;
  152 + }
  153 +
  154 + .jstree-themeicon-custom {
  155 + background-color: transparent;
  156 + background-image: none;
  157 + background-position: 0 0;
  158 +
  159 + &.material-icons {
  160 + margin: 0;
  161 + font-size: 24px;
  162 + }
  163 + }
  164 +
  165 + .jstree-node,
  166 + .jstree-leaf > .jstree-ocl {
  167 + background: 0 0;
  168 + }
  169 +
  170 + .jstree-node {
  171 + min-width: 40px;
  172 + min-height: 40px;
  173 + margin-left: 40px;
  174 + line-height: 40px;
  175 + white-space: nowrap;
  176 + background-repeat: repeat-y;
  177 + background-position: -80px 0;
  178 + }
  179 +
  180 + .jstree-last {
  181 + background: 0 0;
  182 + }
  183 +
  184 + .jstree-anchor {
  185 + height: 40px;
  186 + font-size: 1.1em;
  187 + font-weight: 700;
  188 + line-height: 40px;
  189 + text-shadow: 1px 1px #fff;
  190 + }
  191 +
  192 + .jstree-icon,
  193 + .jstree-icon:empty {
  194 + width: 40px;
  195 + height: 40px;
  196 + line-height: 40px;
  197 + }
  198 +
  199 + > {
  200 + .jstree-container-ul > .jstree-node {
  201 + margin-right: 0;
  202 + margin-left: 0;
  203 + }
  204 + }
  205 +
  206 + .jstree-ocl,
  207 + .jstree-themeicon,
  208 + .jstree-checkbox {
  209 + background-size: 120px 240px;
  210 + }
  211 +
  212 + .jstree-leaf > .jstree-ocl {
  213 + background: 0 0;
  214 + background-position: -40px -120px;
  215 + }
  216 +
  217 + .jstree-last > .jstree-ocl {
  218 + background-position: -40px -160px;
  219 + }
  220 +
  221 + .jstree-open > .jstree-ocl {
  222 + background-position: 0 0 !important;
  223 + }
  224 +
  225 + .jstree-closed > .jstree-ocl {
  226 + background-position: 0 -40px !important;
  227 + }
  228 +
  229 + .jstree-themeicon {
  230 + background-position: -40px -40px;
  231 + }
  232 +
  233 + .jstree-checkbox,
  234 + .jstree-checkbox:hover {
  235 + background-position: -40px -80px;
  236 + }
  237 +
  238 + &.jstree-checkbox-selection {
  239 + .jstree-clicked > .jstree-checkbox,
  240 + .jstree-clicked > .jstree-checkbox:hover {
  241 + background-position: 0 -80px;
  242 + }
  243 + }
  244 +
  245 + .jstree-checked > .jstree-checkbox,
  246 + .jstree-checked > .jstree-checkbox:hover {
  247 + background-position: 0 -80px;
  248 + }
  249 +
  250 + .jstree-anchor > .jstree-undetermined,
  251 + .jstree-anchor > .jstree-undetermined:hover {
  252 + background-position: 0 -120px;
  253 + }
  254 +
  255 + .jstree-striped {
  256 + background: 0 0;
  257 + }
  258 +
  259 + .jstree-wholerow {
  260 + height: 40px;
  261 + background: #ebebeb;
  262 + border-top: 1px solid rgba(255, 255, 255, .7);
  263 + border-bottom: 1px solid rgba(64, 64, 64, .2);
  264 + }
  265 +
  266 + .jstree-wholerow-hovered {
  267 + background: #e7f4f9;
  268 + }
  269 +
  270 + .jstree-wholerow-clicked {
  271 + background: #beebff;
  272 + }
  273 +
  274 + .jstree-children {
  275 + .jstree-last > .jstree-wholerow {
  276 + box-shadow: inset 0 -6px 3px -5px #666;
  277 + }
  278 +
  279 + .jstree-open > .jstree-wholerow {
  280 + border-top: 0;
  281 + box-shadow: inset 0 6px 3px -5px #666;
  282 + }
  283 +
  284 + .jstree-open + .jstree-open {
  285 + box-shadow: none;
  286 + }
  287 + }
  288 +
  289 + &.jstree-rtl {
  290 + .jstree-node {
  291 + margin-right: 40px;
  292 + margin-left: 0;
  293 + }
  294 +
  295 + .jstree-container-ul > .jstree-node {
  296 + margin-right: 0;
  297 + }
  298 +
  299 + .jstree-closed > .jstree-ocl {
  300 + background-position: -40px 0 !important;
  301 + }
  302 + }
  303 + }
  304 + }
  305 +}
  306 +
  307 +.tb-nav-tree .md-button.tb-active {
  308 + font-weight: 500;
  309 + background-color: rgba(255, 255, 255, .15);
  310 +}
  311 +
  312 +.tb-nav-tree,
  313 +.tb-nav-tree ul {
  314 + margin-top: 0;
  315 + list-style: none;
  316 +
  317 + &:first-child {
  318 + padding: 0;
  319 + }
  320 +
  321 + li {
  322 + .md-button {
  323 + width: 100%;
  324 + max-height: 40px;
  325 + padding: 0 16px;
  326 + margin: 0;
  327 + overflow: hidden;
  328 + line-height: 40px;
  329 + color: inherit;
  330 + text-align: left;
  331 + text-decoration: none;
  332 + text-overflow: ellipsis;
  333 + text-transform: none;
  334 + text-rendering: optimizeLegibility;
  335 + white-space: nowrap;
  336 + cursor: pointer;
  337 + border-radius: 0;
  338 +
  339 + span {
  340 + overflow: hidden;
  341 + text-overflow: ellipsis;
  342 + white-space: nowrap;
  343 + }
  344 + }
  345 + }
  346 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-nav-tree-container"></div>
... ...
... ... @@ -27,6 +27,7 @@ import thingsboardApiUser from '../api/user.service';
27 27 import thingsboardNoAnimate from '../components/no-animate.directive';
28 28 import thingsboardOnFinishRender from '../components/finish-render.directive';
29 29 import thingsboardSideMenu from '../components/side-menu.directive';
  30 +import thingsboardNavTree from '../components/nav-tree.directive';
30 31 import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive';
31 32 import thingsboardKvMap from '../components/kv-map.directive';
32 33 import thingsboardJsonObjectEdit from '../components/json-object-edit.directive';
... ... @@ -89,6 +90,7 @@ export default angular.module('thingsboard.home', [
89 90 thingsboardNoAnimate,
90 91 thingsboardOnFinishRender,
91 92 thingsboardSideMenu,
  93 + thingsboardNavTree,
92 94 thingsboardDashboardAutocomplete,
93 95 thingsboardKvMap,
94 96 thingsboardJsonObjectEdit,
... ...
... ... @@ -1566,7 +1566,8 @@
1566 1566 "row-click": "On row click",
1567 1567 "polygon-click": "On polygon click",
1568 1568 "marker-click": "On marker click",
1569   - "tooltip-tag-action": "Tooltip tag action"
  1569 + "tooltip-tag-action": "Tooltip tag action",
  1570 + "node-selected": "On node selected"
1570 1571 }
1571 1572 },
1572 1573 "language": {
... ...
  1 +/*
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import './entities-hierarchy-widget.scss';
  18 +
  19 +/* eslint-disable import/no-unresolved, import/default */
  20 +
  21 +import entitiesHierarchyWidgetTemplate from './entities-hierarchy-widget.tpl.html';
  22 +
  23 +/* eslint-enable import/no-unresolved, import/default */
  24 +
  25 +export default angular.module('thingsboard.widgets.entitiesHierarchyWidget', [])
  26 + .directive('tbEntitiesHierarchyWidget', EntitiesHierarchyWidget)
  27 + .name;
  28 +
  29 +/*@ngInject*/
  30 +function EntitiesHierarchyWidget() {
  31 + return {
  32 + restrict: "E",
  33 + scope: true,
  34 + bindToController: {
  35 + hierarchyId: '=',
  36 + ctx: '='
  37 + },
  38 + controller: EntitiesHierarchyWidgetController,
  39 + controllerAs: 'vm',
  40 + templateUrl: entitiesHierarchyWidgetTemplate
  41 + };
  42 +}
  43 +
  44 +/*@ngInject*/
  45 +function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast, types, entityService, entityRelationService /*$filter, $mdMedia, $mdPanel, $document, $translate, $timeout, utils, types*/) {
  46 + var vm = this;
  47 +
  48 + vm.showData = true;
  49 +
  50 + vm.nodeEditCallbacks = {};
  51 +
  52 + vm.nodeIdCounter = 0;
  53 +
  54 + vm.nodesMap = {};
  55 + vm.pendingUpdateNodeTasks = {};
  56 +
  57 + $scope.$watch('vm.ctx', function() {
  58 + if (vm.ctx && vm.ctx.defaultSubscription) {
  59 + vm.settings = vm.ctx.settings;
  60 + vm.widgetConfig = vm.ctx.widgetConfig;
  61 + vm.subscription = vm.ctx.defaultSubscription;
  62 + vm.datasources = vm.subscription.datasources;
  63 + initializeConfig();
  64 + updateDatasources();
  65 + }
  66 + });
  67 +
  68 + $scope.$on('entities-hierarchy-data-updated', function(event, hierarchyId) {
  69 + if (vm.hierarchyId == hierarchyId) {
  70 + if (vm.subscription) {
  71 + updateNodeData(vm.subscription.data);
  72 + }
  73 + }
  74 + });
  75 +
  76 + vm.onNodesInserted = onNodesInserted;
  77 +
  78 + vm.onNodeSelected = onNodeSelected;
  79 +
  80 + function initializeConfig() {
  81 +
  82 + var testNodeCtx = {
  83 + entity: {
  84 + id: {
  85 + entityType: 'DEVICE',
  86 + id: '123'
  87 + },
  88 + name: 'TEST DEV1'
  89 + },
  90 + data: {},
  91 + level: 2
  92 + };
  93 + var parentNodeCtx = angular.copy(testNodeCtx);
  94 + parentNodeCtx.level = 1;
  95 + testNodeCtx.parentNodeCtx = parentNodeCtx;
  96 +
  97 + var nodeRelationQueryFunction = loadNodeCtxFunction(vm.settings.nodeRelationQueryFunction, 'nodeCtx', testNodeCtx);
  98 + var nodeIconFunction = loadNodeCtxFunction(vm.settings.nodeIconFunction, 'nodeCtx', testNodeCtx);
  99 + var nodeTextFunction = loadNodeCtxFunction(vm.settings.nodeTextFunction, 'nodeCtx', testNodeCtx);
  100 + var nodeDisabledFunction = loadNodeCtxFunction(vm.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx);
  101 + var nodeHasChildrenFunction = loadNodeCtxFunction(vm.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx);
  102 +
  103 + var testNodeCtx2 = angular.copy(testNodeCtx);
  104 + testNodeCtx2.entity.name = 'TEST DEV2';
  105 +
  106 + var nodesSortFunction = loadNodeCtxFunction(vm.settings.nodesSortFunction, 'nodeCtx1,nodeCtx2', testNodeCtx, testNodeCtx2);
  107 +
  108 + vm.nodeRelationQueryFunction = nodeRelationQueryFunction || defaultNodeRelationQueryFunction;
  109 + vm.nodeIconFunction = nodeIconFunction || defaultNodeIconFunction;
  110 + vm.nodeTextFunction = nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name);
  111 + vm.nodeDisabledFunction = nodeDisabledFunction || (() => false);
  112 + vm.nodeHasChildrenFunction = nodeHasChildrenFunction || (() => true);
  113 + vm.nodesSortFunction = nodesSortFunction || defaultSortFunction;
  114 + }
  115 +
  116 + function loadNodeCtxFunction(functionBody, argNames, ...args) {
  117 + var nodeCtxFunction = null;
  118 + if (angular.isDefined(functionBody) && functionBody.length) {
  119 + try {
  120 + nodeCtxFunction = new Function(argNames, functionBody);
  121 + var res = nodeCtxFunction.apply(null, args);
  122 + if (angular.isUndefined(res)) {
  123 + nodeCtxFunction = null;
  124 + }
  125 + } catch (e) {
  126 + nodeCtxFunction = null;
  127 + }
  128 + }
  129 + return nodeCtxFunction;
  130 + }
  131 +
  132 + function updateDatasources() {
  133 + vm.loadNodes = loadNodes;
  134 + }
  135 +
  136 + function onNodesInserted(nodes/*, parent*/) {
  137 + if (nodes) {
  138 + nodes.forEach((nodeId) => {
  139 + var task = vm.pendingUpdateNodeTasks[nodeId];
  140 + if (task) {
  141 + task();
  142 + delete vm.pendingUpdateNodeTasks[nodeId];
  143 + }
  144 + });
  145 + }
  146 + }
  147 +
  148 + function onNodeSelected(node, event) {
  149 + var nodeId;
  150 + if (!node) {
  151 + nodeId = -1;
  152 + } else {
  153 + nodeId = node.id;
  154 + }
  155 + if (nodeId !== -1) {
  156 + var selectedNode = vm.nodesMap[nodeId];
  157 + if (selectedNode) {
  158 + var descriptors = vm.ctx.actionsApi.getActionDescriptors('nodeSelected');
  159 + if (descriptors.length) {
  160 + var entity = selectedNode.data.nodeCtx.entity;
  161 + vm.ctx.actionsApi.handleWidgetAction(event, descriptors[0], entity.id, entity.name, { nodeCtx: selectedNode.data.nodeCtx });
  162 + }
  163 + }
  164 + }
  165 + }
  166 +
  167 + function updateNodeData(subscriptionData) {
  168 + var affectedNodes = [];
  169 + if (subscriptionData) {
  170 + for (var i=0;i<subscriptionData.length;i++) {
  171 + var datasource = subscriptionData[i].datasource;
  172 + if (datasource.nodeId) {
  173 + var node = vm.nodesMap[datasource.nodeId];
  174 + var key = subscriptionData[i].dataKey.label;
  175 + var value = undefined;
  176 + if (subscriptionData[i].data && subscriptionData[i].data.length) {
  177 + value = subscriptionData[i].data[0][1];
  178 + }
  179 + if (node.data.nodeCtx.data[key] !== value) {
  180 + if (affectedNodes.indexOf(datasource.nodeId) === -1) {
  181 + affectedNodes.push(datasource.nodeId);
  182 + }
  183 + node.data.nodeCtx.data[key] = value;
  184 + }
  185 + }
  186 + }
  187 + }
  188 + affectedNodes.forEach((nodeId) => {
  189 + var node = vm.nodeEditCallbacks.getNode(nodeId);
  190 + if (node) {
  191 + updateNodeStyle(vm.nodesMap[nodeId]);
  192 + } else {
  193 + vm.pendingUpdateNodeTasks[nodeId] = () => {
  194 + updateNodeStyle(vm.nodesMap[nodeId]);
  195 + };
  196 + }
  197 + });
  198 + }
  199 +
  200 + function updateNodeStyle(node) {
  201 + var newText = prepareNodeText(node);
  202 + if (!angular.equals(node.text, newText)) {
  203 + node.text = newText;
  204 + vm.nodeEditCallbacks.updateNode(node.id, node.text);
  205 + }
  206 + var newDisabled = vm.nodeDisabledFunction(node.data.nodeCtx);
  207 + if (!angular.equals(node.state.disabled, newDisabled)) {
  208 + node.state.disabled = newDisabled;
  209 + if (node.state.disabled) {
  210 + vm.nodeEditCallbacks.disableNode(node.id);
  211 + } else {
  212 + vm.nodeEditCallbacks.enableNode(node.id);
  213 + }
  214 + }
  215 + var newHasChildren = vm.nodeHasChildrenFunction(node.data.nodeCtx);
  216 + if (!angular.equals(node.children, newHasChildren)) {
  217 + node.children = newHasChildren;
  218 + vm.nodeEditCallbacks.setNodeHasChildren(node.id, node.children);
  219 + }
  220 + }
  221 +
  222 + function prepareNodeText(node) {
  223 + var nodeIcon = prepareNodeIcon(node.data.nodeCtx);
  224 + var nodeText = vm.nodeTextFunction(node.data.nodeCtx);
  225 + return nodeIcon + nodeText;
  226 + }
  227 +
  228 + function loadNodes(node, cb) {
  229 + if (node.id === '#') {
  230 + var tasks = [];
  231 + for (var i=0;i<vm.datasources.length;i++) {
  232 + var datasource = vm.datasources[i];
  233 + tasks.push(datasourceToNode(datasource));
  234 + }
  235 + $q.all(tasks).then((nodes) => {
  236 + cb(prepareNodes(nodes));
  237 + updateNodeData(vm.subscription.data);
  238 + });
  239 + } else {
  240 + if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.nodeCtx.entity.id.entityType !== 'function') {
  241 + var relationQuery = prepareNodeRelationQuery(node.data.nodeCtx);
  242 + entityRelationService.findByQuery(relationQuery, {ignoreErrors: true, ignoreLoading: true}).then(
  243 + (entityRelations) => {
  244 + var tasks = [];
  245 + for (var i=0;i<entityRelations.length;i++) {
  246 + var relation = entityRelations[i];
  247 + var targetId = relationQuery.parameters.direction === types.entitySearchDirection.from ? relation.to : relation.from;
  248 + tasks.push(entityIdToNode(targetId.entityType, targetId.id, node.data.datasource, node.data.nodeCtx));
  249 + }
  250 + $q.all(tasks).then((nodes) => {
  251 + cb(prepareNodes(nodes));
  252 + });
  253 + },
  254 + (error) => {
  255 + var errorText = "Failed to get relations!";
  256 + if (error && error.status === 400) {
  257 + errorText = "Invalid relations query returned by 'Node relations query function'! Please check widget configuration!";
  258 + }
  259 + showError(errorText);
  260 + }
  261 + );
  262 + } else {
  263 + cb([]);
  264 + }
  265 + }
  266 + }
  267 +
  268 + function showError(errorText) {
  269 + var toastParent = angular.element('.tb-entities-hierarchy', $element);
  270 + toast.showError(errorText, toastParent, 'bottom left');
  271 + }
  272 +
  273 + function prepareNodes(nodes) {
  274 + nodes = nodes.filter((node) => node !== null);
  275 + nodes.sort((node1, node2) => vm.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx));
  276 + return nodes;
  277 + }
  278 +
  279 + function datasourceToNode(datasource, parentNodeCtx) {
  280 + var deferred = $q.defer();
  281 + resolveEntity(datasource).then(
  282 + (entity) => {
  283 + if (entity != null) {
  284 + var node = {
  285 + id: ++vm.nodeIdCounter
  286 + };
  287 + vm.nodesMap[node.id] = node;
  288 + datasource.nodeId = node.id;
  289 + node.icon = false;
  290 + var nodeCtx = {
  291 + parentNodeCtx: parentNodeCtx,
  292 + entity: entity,
  293 + data: {}
  294 + };
  295 + nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1;
  296 + node.data = {
  297 + datasource: datasource,
  298 + nodeCtx: nodeCtx
  299 + };
  300 + node.state = {
  301 + disabled: vm.nodeDisabledFunction(node.data.nodeCtx)
  302 + };
  303 + node.text = prepareNodeText(node);
  304 + node.children = vm.nodeHasChildrenFunction(node.data.nodeCtx);
  305 + deferred.resolve(node);
  306 + } else {
  307 + deferred.resolve(null);
  308 + }
  309 + }
  310 + );
  311 + return deferred.promise;
  312 + }
  313 +
  314 + function entityIdToNode(entityType, entityId, parentDatasource, parentNodeCtx) {
  315 + var deferred = $q.defer();
  316 + var datasource = {
  317 + dataKeys: parentDatasource.dataKeys,
  318 + type: types.datasourceType.entity,
  319 + entityType: entityType,
  320 + entityId: entityId
  321 + };
  322 + datasourceToNode(datasource, parentNodeCtx).then(
  323 + (node) => {
  324 + if (node != null) {
  325 + var subscriptionOptions = {
  326 + type: types.widgetType.latest.value,
  327 + datasources: [datasource],
  328 + callbacks: {
  329 + onDataUpdated: (subscription) => {
  330 + updateNodeData(subscription.data);
  331 + }
  332 + }
  333 + };
  334 + vm.ctx.subscriptionApi.createSubscription(subscriptionOptions, true).then(
  335 + (/*subscription*/) => {
  336 + deferred.resolve(node);
  337 + }
  338 + );
  339 + } else {
  340 + deferred.resolve(node);
  341 + }
  342 + }
  343 + );
  344 + return deferred.promise;
  345 + }
  346 +
  347 + function resolveEntity(datasource) {
  348 + var deferred = $q.defer();
  349 + if (datasource.type === types.datasourceType.function) {
  350 + var entity = {
  351 + id: {
  352 + entityType: "function"
  353 + },
  354 + name: datasource.name
  355 + }
  356 + deferred.resolve(entity);
  357 + } else {
  358 + entityService.getEntity(datasource.entityType, datasource.entityId, {ignoreLoading: true}).then(
  359 + (entity) => {
  360 + deferred.resolve(entity);
  361 + },
  362 + () => {
  363 + deferred.resolve(null);
  364 + }
  365 + );
  366 + }
  367 + return deferred.promise;
  368 + }
  369 +
  370 +
  371 + function prepareNodeRelationQuery(nodeCtx) {
  372 + var relationQuery = vm.nodeRelationQueryFunction(nodeCtx);
  373 + if (relationQuery && relationQuery === 'default') {
  374 + relationQuery = defaultNodeRelationQueryFunction(nodeCtx);
  375 + }
  376 + return relationQuery;
  377 + }
  378 +
  379 + function defaultNodeRelationQueryFunction(nodeCtx) {
  380 + var entity = nodeCtx.entity;
  381 + var query = {
  382 + parameters: {
  383 + rootId: entity.id.id,
  384 + rootType: entity.id.entityType,
  385 + direction: types.entitySearchDirection.from,
  386 + relationTypeGroup: "COMMON",
  387 + maxLevel: 1
  388 + },
  389 + filters: [
  390 + {
  391 + relationType: "Contains",
  392 + entityTypes: []
  393 + }
  394 + ]
  395 + };
  396 + return query;
  397 + }
  398 +
  399 + function prepareNodeIcon(nodeCtx) {
  400 + var iconInfo = vm.nodeIconFunction(nodeCtx);
  401 + if (iconInfo && iconInfo === 'default') {
  402 + iconInfo = defaultNodeIconFunction(nodeCtx);
  403 + }
  404 + if (iconInfo && (iconInfo.iconUrl || iconInfo.materialIcon)) {
  405 + if (iconInfo.materialIcon) {
  406 + return materialIconHtml(iconInfo.materialIcon);
  407 + } else {
  408 + return iconUrlHtml(iconInfo.iconUrl);
  409 + }
  410 + } else {
  411 + return "";
  412 + }
  413 + }
  414 +
  415 + function materialIconHtml(materialIcon) {
  416 + return '<md-icon aria-label="'+materialIcon+'" class="node-icon material-icons" role="img" aria-hidden="false">'+materialIcon+'</md-icon>';
  417 + }
  418 +
  419 + function iconUrlHtml(iconUrl) {
  420 + return '<div class="node-icon" style="background-image: url('+iconUrl+');">&nbsp;</div>';
  421 + }
  422 +
  423 + function defaultNodeIconFunction(nodeCtx) {
  424 + var materialIcon = 'insert_drive_file';
  425 + var entity = nodeCtx.entity;
  426 + if (entity && entity.id && entity.id.entityType) {
  427 + switch (entity.id.entityType) {
  428 + case 'function':
  429 + materialIcon = 'functions';
  430 + break;
  431 + case types.entityType.device:
  432 + materialIcon = 'devices_other';
  433 + break;
  434 + case types.entityType.asset:
  435 + materialIcon = 'domain';
  436 + break;
  437 + case types.entityType.tenant:
  438 + materialIcon = 'supervisor_account';
  439 + break;
  440 + case types.entityType.customer:
  441 + materialIcon = 'supervisor_account';
  442 + break;
  443 + case types.entityType.user:
  444 + materialIcon = 'account_circle';
  445 + break;
  446 + case types.entityType.dashboard:
  447 + materialIcon = 'dashboards';
  448 + break;
  449 + case types.entityType.alarm:
  450 + materialIcon = 'notifications_active';
  451 + break;
  452 + case types.entityType.entityView:
  453 + materialIcon = 'view_quilt';
  454 + break;
  455 + }
  456 + }
  457 + return {
  458 + materialIcon: materialIcon
  459 + };
  460 + }
  461 +
  462 + function defaultSortFunction(nodeCtx1, nodeCtx2) {
  463 + var result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);
  464 + if (result === 0) {
  465 + result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);
  466 + }
  467 + return result;
  468 + }
  469 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.tb-entities-hierarchy {
  18 + .tb-entities-nav-tree-panel {
  19 + overflow-x: auto;
  20 + overflow-y: auto;
  21 +
  22 + .tb-nav-tree-container {
  23 + &.jstree-proton {
  24 + .jstree-anchor {
  25 + div.node-icon {
  26 + display: inline-block;
  27 + width: 22px;
  28 + height: 22px;
  29 + margin-right: 2px;
  30 + margin-bottom: 2px;
  31 + background-color: transparent;
  32 + background-repeat: no-repeat;
  33 + background-attachment: scroll;
  34 + background-position: center center;
  35 + background-size: 18px 18px;
  36 + }
  37 +
  38 + md-icon.node-icon {
  39 + width: 22px;
  40 + min-width: 22px;
  41 + height: 22px;
  42 + min-height: 22px;
  43 + margin-right: 2px;
  44 + margin-bottom: 2px;
  45 + color: inherit;
  46 +
  47 + &.material-icons { /* stylelint-disable-line selector-max-class */
  48 + font-size: 18px;
  49 + line-height: 22px;
  50 + text-align: center;
  51 + }
  52 + }
  53 +
  54 + &.jstree-hovered:not(.jstree-clicked),
  55 + &.jstree-disabled {
  56 + div.node-icon { /* stylelint-disable-line selector-max-class */
  57 + opacity: .5;
  58 + }
  59 + }
  60 + }
  61 + }
  62 + }
  63 + }
  64 +}
  65 +
  66 +@media (max-width: 768px) {
  67 + .tb-entities-hierarchy {
  68 + .tb-entities-nav-tree-panel {
  69 + .tb-nav-tree-container {
  70 + &.jstree-proton-responsive {
  71 + .jstree-anchor {
  72 + div.node-icon {
  73 + width: 40px;
  74 + height: 40px;
  75 + margin: 0;
  76 + background-size: 24px 24px;
  77 + }
  78 +
  79 + md-icon.node-icon {
  80 + width: 40px;
  81 + min-width: 40px;
  82 + height: 40px;
  83 + min-height: 40px;
  84 + margin: 0;
  85 +
  86 + &.material-icons { /* stylelint-disable-line selector-max-class */
  87 + font-size: 24px;
  88 + line-height: 40px;
  89 + }
  90 + }
  91 + }
  92 + }
  93 + }
  94 + }
  95 + }
  96 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-absolute-fill tb-entities-hierarchy" layout="column">
  19 + <div ng-show="vm.showData" flex class="tb-absolute-fill" layout="column">
  20 + <div flex class="tb-entities-nav-tree-panel">
  21 + <tb-nav-tree
  22 + load-nodes="vm.loadNodes"
  23 + on-node-selected="vm.onNodeSelected(node, event)"
  24 + on-nodes-inserted="vm.onNodesInserted(nodes, parent)"
  25 + edit-callbacks="vm.nodeEditCallbacks"
  26 + ></tb-nav-tree>
  27 + </div>
  28 + </div>
  29 +</div>
... ...