From 52218bf875fea4623bd3907055729c9fe3f3f427 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 8 Jan 2021 14:46:58 +0200 Subject: [PATCH] Edge widgets initial implementation --- .../system/widget_bundles/edge_widgets.json | 25 + .../install/ThingsboardInstallService.java | 1 + ui/src/app/api/widget.service.js | 3 +- ui/src/app/components/nav-tree.scss | 12 + .../app/widget/lib/edges-overview-widget.js | 483 ++++++++++++++++++ .../app/widget/lib/edges-overview-widget.scss | 109 ++++ .../widget/lib/edges-overview-widget.tpl.html | 26 + 7 files changed, 658 insertions(+), 1 deletion(-) create mode 100644 application/src/main/data/json/system/widget_bundles/edge_widgets.json create mode 100644 ui/src/app/widget/lib/edges-overview-widget.js create mode 100644 ui/src/app/widget/lib/edges-overview-widget.scss create mode 100644 ui/src/app/widget/lib/edges-overview-widget.tpl.html diff --git a/application/src/main/data/json/system/widget_bundles/edge_widgets.json b/application/src/main/data/json/system/widget_bundles/edge_widgets.json new file mode 100644 index 0000000000..32047f20a4 --- /dev/null +++ b/application/src/main/data/json/system/widget_bundles/edge_widgets.json @@ -0,0 +1,25 @@ +{ + "widgetsBundle": { + "alias": "edge_widgets", + "title": "Edge widgets", + "image": null + }, + "widgetTypes": [ + { + "alias": "edges_overview", + "name": "Edges Quick Overview", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n};\n\nself.onDestroy = function() {\n};\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{},\"title\":\"Edges Quick Overview\",\"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\":{}}" + } + } + ] +} diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index c9f6b94922..91a3912fbe 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -176,6 +176,7 @@ public class ThingsboardInstallService { systemDataLoaderService.deleteSystemWidgetBundle("input_widgets"); systemDataLoaderService.deleteSystemWidgetBundle("date"); systemDataLoaderService.deleteSystemWidgetBundle("entity_admin_widgets"); + systemDataLoaderService.deleteSystemWidgetBundle("edge_widgets"); systemDataLoaderService.loadSystemWidgets(); break; diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js index 4b706e827d..742f102d28 100644 --- a/ui/src/app/api/widget.service.js +++ b/ui/src/app/api/widget.service.js @@ -22,6 +22,7 @@ import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-wid import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget'; import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget'; import thingsboardEntitiesHierarchyWidget from '../widget/lib/entities-hierarchy-widget'; +import thingsboardEdgesOverviewWidget from '../widget/lib/edges-overview-widget' import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget'; import thingsboardDateRangeNavigatorWidget from '../widget/lib/date-range-navigator/date-range-navigator'; import thingsboardMultipleInputWidget from '../widget/lib/multiple-input-widget'; @@ -52,7 +53,7 @@ import thingsboardUtils from '../common/utils.service'; export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, - thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget, + thingsboardEntitiesHierarchyWidget, thingsboardEdgesOverviewWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget, thingsboardMultipleInputWidget, thingsboardWebCameraInputWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, thingsboardJsonToString, TripAnimationWidget]) .factory('widgetService', WidgetService) diff --git a/ui/src/app/components/nav-tree.scss b/ui/src/app/components/nav-tree.scss index df903e747b..dc21431479 100644 --- a/ui/src/app/components/nav-tree.scss +++ b/ui/src/app/components/nav-tree.scss @@ -131,6 +131,18 @@ content: "supervisor_account"; } } + + &.tb-edge-group { + &::before { + content: "router"; + } + } + + &.tb-rule-chain-group { + &::before { + content: "settings_ethernet"; + } + } } } } diff --git a/ui/src/app/widget/lib/edges-overview-widget.js b/ui/src/app/widget/lib/edges-overview-widget.js new file mode 100644 index 0000000000..278ccb52fb --- /dev/null +++ b/ui/src/app/widget/lib/edges-overview-widget.js @@ -0,0 +1,483 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import './edges-overview-widget.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import edgesOverviewWidgetTemplate from './edges-overview-widget.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +export default angular.module('thingsboard.widgets.edgesOverviewWidget', []) + .directive('tbEdgesOverviewWidget', EdgesOverviewWidget) + .name; +/* eslint-disable no-unused-vars, no-undef */ +/*@ngInject*/ +function EdgesOverviewWidget() { + return { + restrict: "E", + scope: true, + bindToController: { + hierarchyId: '=', + ctx: '=' + }, + controller: EdgesOverviewWidgetController, + controllerAs: 'vm', + templateUrl: edgesOverviewWidgetTemplate + }; +} + +/*@ngInject*/ +function EdgesOverviewWidgetController($element, $scope, $q, $timeout, toast, types, entityService, userService, entityRelationService, + assetService, deviceService, entityViewService, dashboardService, ruleChainService /*$filter, $mdMedia, $mdPanel, $document, $translate, $timeout, utils, types*/) { + var vm = this; + + vm.showData = true; + + vm.nodeEditCallbacks = {}; + + vm.nodeIdCounter = 0; + + vm.nodesMap = {}; + vm.entityGroupsNodesMap = {}; + vm.pendingUpdateNodeTasks = {}; + + vm.query = { + search: null + }; + + vm.edgeGroupsTypes = [ + types.entityType.asset, + types.entityType.device, + types.entityType.entityView, + types.entityType.dashboard, + types.entityType.rulechain, + ] + + vm.onNodesInserted = onNodesInserted; + vm.onNodeSelected = onNodeSelected; + + $scope.$watch('vm.ctx', function() { + if (vm.ctx && vm.ctx.defaultSubscription) { + vm.settings = vm.ctx.settings; + vm.widgetConfig = vm.ctx.widgetConfig; + vm.subscription = vm.ctx.defaultSubscription; + vm.datasources = vm.subscription.datasources; + updateDatasources(); + } + }); + + $scope.$on('edges-overview-data-updated', function(event, hierarchyId) { + if (vm.hierarchyId == hierarchyId) { + if (vm.subscription) { + updateNodeData(vm.subscription.data); + } + } + }); + + function updateDatasources() { + vm.loadNodes = loadNodes; + } + + function onNodesInserted(nodes/*, parent*/) { + if (nodes) { + nodes.forEach((nodeId) => { + var task = vm.pendingUpdateNodeTasks[nodeId]; + if (task) { + task(); + delete vm.pendingUpdateNodeTasks[nodeId]; + } + }); + } + } + + function onNodeSelected(node, event) { + var nodeId; + if (!node) { + nodeId = -1; + } else { + nodeId = node.id; + } + if (nodeId !== -1) { + var selectedNode = vm.nodesMap[nodeId]; + if (selectedNode) { + var descriptors = vm.ctx.actionsApi.getActionDescriptors('nodeSelected'); + if (descriptors.length) { + var entity = selectedNode.data.nodeCtx.entity; + vm.ctx.actionsApi.handleWidgetAction(event, descriptors[0], entity.id, entity.name, { nodeCtx: selectedNode.data.nodeCtx }); + } + } + } + } + + function updateNodeData(subscriptionData) { + var affectedNodes = []; + if (subscriptionData) { + for (var i=0;i { + var node = vm.nodeEditCallbacks.getNode(nodeId); + if (node) { + updateNodeStyle(vm.nodesMap[nodeId]); + } else { + vm.pendingUpdateNodeTasks[nodeId] = () => { + updateNodeStyle(vm.nodesMap[nodeId]); + }; + } + }); + } + + function updateNodeStyle(node) { + var newText = prepareNodeText(node); + if (!angular.equals(node.text, newText)) { + node.text = newText; + vm.nodeEditCallbacks.updateNode(node.id, node.text); + } + var newDisabled = vm.nodeDisabledFunction(node.data.nodeCtx); + if (!angular.equals(node.state.disabled, newDisabled)) { + node.state.disabled = newDisabled; + if (node.state.disabled) { + vm.nodeEditCallbacks.disableNode(node.id); + } else { + vm.nodeEditCallbacks.enableNode(node.id); + } + } + var newHasChildren = vm.nodeHasChildrenFunction(node.data.nodeCtx); + if (!angular.equals(node.children, newHasChildren)) { + node.children = newHasChildren; + vm.nodeEditCallbacks.setNodeHasChildren(node.id, node.children); + } + } + + function prepareNodeText(node) { + var nodeIcon = prepareNodeIcon(node.data.nodeCtx); + var nodeText = vm.nodeTextFunction(node.data.nodeCtx); + node.data.searchText = nodeText ? nodeText.replace(/<[^>]+>/g, '').toLowerCase() : ""; + return nodeIcon + nodeText; + } + + function prepareEgeGroupText(node) { + var nodeIcon = prepareNodeIcon(node.data.nodeCtx); + var nodeText = "Edge rule chains"; + node.data.searchText = nodeText ? nodeText.replace(/<[^>]+>/g, '').toLowerCase() : ""; + return nodeIcon + nodeText; + } + + function loadNodes(node, cb) { + var datasource = vm.datasources[0]; + if (node.id === '#' && datasource) { + if (datasource.type === 'entity' && datasource.entity.id.entityType === types.entityType.edge) { + var selectedEdge = datasource.entity; + vm.ctx.widgetTitle = selectedEdge.name; + cb(loadNodesForEdge(selectedEdge.id.id, selectedEdge)) + } + } else { + cb([]); + } + } + + function loadNodesForEdge(parentNodeId, entity) { + var nodes = []; + var nodesMap = {}; + vm.entityGroupsNodesMap[parentNodeId] = nodesMap; + var user = userService.getCurrentUser(); + var allowedGroupTypes = vm.edgeGroupsTypes; + if (user.authority === 'CUSTOMER_USER') { + allowedGroupTypes = vm.edgeGroupsTypes.filter(type => type !== types.entityType.rulechain) + } + allowedGroupTypes.forEach((groupType) => { + var node = { + id: ++vm.nodeIdCounter, + icon: 'material-icons ' + iconForGroupType(groupType), + text: textForGroupType(groupType), + children: true, + data: { + groupType, + entity, + internalId: entity.id.id + '_' + groupType + } + }; + nodes.push(node); + nodesMap[groupType] = node.id; + }); + return nodes; + } + + function iconForGroupType(groupType) { + switch (groupType) { + case vm.types.entityType.asset: + return 'tb-asset-group'; + case vm.types.entityType.device: + return 'tb-device-group'; + case vm.types.entityType.entityView: + return 'tb-entity-view-group'; + case vm.types.entityType.dashboard: + return 'tb-dashboard-group'; + case vm.types.entityType.rulechain: + return 'tb-rule-chain-group'; + } + return ''; + } + + function textForGroupType(groupType) { + switch (groupType) { + case vm.types.entityType.asset: + return $translate.instant('asset.assets'); + case vm.types.entityType.device: + return $translate.instant('device.devices'); + case vm.types.entityType.entityView: + return $translate.instant('entity-view.entity-views'); + case vm.types.entityType.rulechain: + return $translate.instant('rulechain.rulechains'); + case vm.types.entityType.dashboard: + return $translate.instant('dashboard.dashboards'); + } + return ''; + } + + function edgeGroupsNodeText(entityType) { + + } + + function prepareNodes(nodes) { + nodes = nodes.filter((node) => node !== null); + nodes.sort((node1, node2) => vm.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx)); + return nodes; + } + + function datasourceToNode(datasource, parentNodeCtx) { + var deferred = $q.defer(); + resolveEntity(datasource).then( + (entity) => { + if (entity != null) { + var node = { + id: ++vm.nodeIdCounter + }; + vm.nodesMap[node.id] = node; + datasource.nodeId = node.id; + node.icon = false; + var nodeCtx = { + parentNodeCtx: parentNodeCtx, + entity: entity, + data: {} + }; + nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1; + node.data = { + datasource: datasource, + nodeCtx: nodeCtx + }; + node.state = { + disabled: vm.nodeDisabledFunction(node.data.nodeCtx), + opened: vm.nodeOpenedFunction(node.data.nodeCtx) + }; + node.text = prepareNodeText(node); + node.children = vm.nodeHasChildrenFunction(node.data.nodeCtx); + deferred.resolve(node); + } else { + deferred.resolve(null); + } + } + ); + return deferred.promise; + } + + function edgeGroupToNode(entityType, parentDatasource, parentNodeCtx) { + var deferred = $q.defer(); + deferred.resolve(getEdgeGroupNode(entityType, parentDatasource, parentNodeCtx)); + return deferred.promise; + } + + function getEdgeGroupNode(entityType, parentDatasource, parentNodeCtx) { + var datasource = { + dataKeys: parentDatasource.dataKeys, + entityId: parentDatasource.entity.id.id + '_' + entityType, + type: "edgeGroup", + entityType: entityType + }; + + var node = { + id: ++vm.nodeIdCounter + }; + vm.nodesMap[node.id] = node; + datasource.nodeId = node.id; + node.icon = false; + var nodeCtx = { + parentNodeCtx: 3, + data: {}, + entity: { + id: { + id: parentDatasource.entity.id.id + '_' + entityType, + entityType: 'group' + }, + name: "Edge Group RC" + } + }; + nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1; + node.data = { + datasource: datasource, + nodeCtx: nodeCtx + }; + node.state = { + disabled: vm.nodeDisabledFunction(node.data.nodeCtx), + opened: vm.nodeOpenedFunction(node.data.nodeCtx) + }; + node.text = prepareEgeGroupText(node); + node.children = vm.nodeHasChildrenFunction(node.data.nodeCtx); + return node; + } + + function entityIdToEdgeGroupNode(entityType, entityId, parentDatasource, parentNodeCtx) { + var deferred = $q.defer(); + var datasource = { + dataKeys: parentDatasource.dataKeys, + type: types.datasourceType.entity, + entityType: entityType, + entityId: entityId + }; + datasourceToNode(datasource, parentNodeCtx).then( + (node) => { + if (node != null) { + var subscriptionOptions = { + type: types.widgetType.latest.value, + datasources: [datasource], + callbacks: { + onDataUpdated: (subscription) => { + updateNodeData(subscription.data); + } + } + }; + vm.ctx.subscriptionApi.createSubscription(subscriptionOptions, true).then( + (/*subscription*/) => { + deferred.resolve(node); + } + ); + } else { + deferred.resolve(node); + } + } + ); + return deferred.promise; + } + + function resolveEntity(datasource) { + var deferred = $q.defer(); + if (datasource.type === types.datasourceType.function) { + var entity = { + id: { + entityType: "function" + }, + name: datasource.name + } + deferred.resolve(entity); + } else { + entityService.getEntity(datasource.entityType, datasource.entityId, {ignoreLoading: true}).then( + (entity) => { + deferred.resolve(entity); + }, + () => { + deferred.resolve(null); + } + ); + } + return deferred.promise; + } + + function prepareNodeIcon(nodeCtx) { + var iconInfo = vm.nodeIconFunction(nodeCtx); + if (iconInfo && iconInfo === 'default') { + iconInfo = defaultNodeIconFunction(nodeCtx); + } + if (iconInfo && (iconInfo.iconUrl || iconInfo.materialIcon)) { + if (iconInfo.materialIcon) { + return materialIconHtml(iconInfo.materialIcon); + } else { + return iconUrlHtml(iconInfo.iconUrl); + } + } else { + return ""; + } + } + + function materialIconHtml(materialIcon) { + return ''+materialIcon+''; + } + + function iconUrlHtml(iconUrl) { + return '
 
'; + } + + function defaultNodeIconFunction(nodeCtx) { + var materialIcon = 'insert_drive_file'; + var entity = nodeCtx.entity; + if (entity && entity.id && entity.id.entityType) { + switch (entity.id.entityType) { + case 'function': + materialIcon = 'functions'; + break; + case types.entityType.device: + materialIcon = 'devices_other'; + break; + case types.entityType.asset: + materialIcon = 'domain'; + break; + case types.entityType.tenant: + materialIcon = 'supervisor_account'; + break; + case types.entityType.customer: + materialIcon = 'supervisor_account'; + break; + case types.entityType.user: + materialIcon = 'account_circle'; + break; + case types.entityType.dashboard: + materialIcon = 'dashboards'; + break; + case types.entityType.alarm: + materialIcon = 'notifications_active'; + break; + case types.entityType.entityView: + materialIcon = 'view_quilt'; + break; + case types.entityType.edge: + materialIcon = 'router'; + break; + case types.entityType.rulechain: + materialIcon = 'settings_ethernet'; + break; + } + } + return { + materialIcon: materialIcon + }; + } + +} diff --git a/ui/src/app/widget/lib/edges-overview-widget.scss b/ui/src/app/widget/lib/edges-overview-widget.scss new file mode 100644 index 0000000000..cd2d419ef2 --- /dev/null +++ b/ui/src/app/widget/lib/edges-overview-widget.scss @@ -0,0 +1,109 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-has-timewindow { + .tb-edges-overview { + md-toolbar { + min-height: 60px; + max-height: 60px; + } + } +} + +.tb-edges-overview { + md-toolbar { + min-height: 39px; + max-height: 39px; + } + + .tb-entities-nav-tree-panel { + overflow-x: auto; + overflow-y: auto; + + .tb-nav-tree-container { + &.jstree-proton { + .jstree-anchor { + div.node-icon { + display: inline-block; + width: 22px; + height: 22px; + margin-right: 2px; + margin-bottom: 2px; + background-color: transparent; + background-repeat: no-repeat; + background-attachment: scroll; + background-position: center center; + background-size: 18px 18px; + } + + md-icon.node-icon { + width: 22px; + min-width: 22px; + height: 22px; + min-height: 22px; + margin-right: 2px; + margin-bottom: 2px; + color: inherit; + + &.material-icons { /* stylelint-disable-line selector-max-class */ + font-size: 18px; + line-height: 22px; + text-align: center; + } + } + + &.jstree-hovered:not(.jstree-clicked), + &.jstree-disabled { + div.node-icon { /* stylelint-disable-line selector-max-class */ + opacity: .5; + } + } + } + } + } + } +} + +@media (max-width: 768px) { + .tb-edges-overview { + .tb-entities-nav-tree-panel { + .tb-nav-tree-container { + &.jstree-proton-responsive { + .jstree-anchor { + div.node-icon { + width: 40px; + height: 40px; + margin: 0; + background-size: 24px 24px; + } + + md-icon.node-icon { + width: 40px; + min-width: 40px; + height: 40px; + min-height: 40px; + margin: 0; + + &.material-icons { /* stylelint-disable-line selector-max-class */ + font-size: 24px; + line-height: 40px; + } + } + } + } + } + } + } +} diff --git a/ui/src/app/widget/lib/edges-overview-widget.tpl.html b/ui/src/app/widget/lib/edges-overview-widget.tpl.html new file mode 100644 index 0000000000..feeefd1013 --- /dev/null +++ b/ui/src/app/widget/lib/edges-overview-widget.tpl.html @@ -0,0 +1,26 @@ + +
+
+
+ +
+
+