Showing
16 changed files
with
1189 additions
and
7 deletions
... | ... | @@ -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'; | ... | ... |
ui/src/app/components/nav-tree.directive.js
0 → 100644
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 | +} | ... | ... |
ui/src/app/components/nav-tree.scss
0 → 100644
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 | +} | ... | ... |
ui/src/app/components/nav-tree.tpl.html
0 → 100644
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+');"> </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> | ... | ... |
ui/src/png/jstree/32px.png
0 → 100644
19 KB
ui/src/png/jstree/40px.png
0 → 100644
1.84 KB