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 index 32047f20a4..47cf3c94aa 100644 --- a/application/src/main/data/json/system/widget_bundles/edge_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/edge_widgets.json @@ -13,12 +13,12 @@ "sizeX": 7.5, "sizeY": 5, "resources": [], - "templateHtml": "\n", + "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\":{}}" + "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('edges-overview-data-updated', self.ctx.$scope.hierarchyId);\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'nodeSelected': {\n name: 'widget-action.node-selected',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EdgesOverviewSettings\",\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 \"nodeOpenedFunction\": {\n \"title\": \"Default node opened 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\": \"nodeOpenedFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeDisabledFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeIconFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeTextFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodesSortFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\n}", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"nodeRelationQueryFunction\":\"/**\\n\\n// Function should return relations query object for current node used to fetch entity children.\\n// Function can return 'default' string value. In this case default relations query will be used.\\n\\n// The following example code will construct simple relations query that will fetch relations of type 'Contains'\\n// from the current entity.\\n\\nvar entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: types.entitySearchDirection.from,\\n relationTypeGroup: \\\"COMMON\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n**/\\n\",\"nodeHasChildrenFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node has children (whether it can be expanded).\\n\\n// The following example code will restrict entities hierarchy expansion up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n// The next example code will restrict entities expansion according to the value of example 'nodeHasChildren' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeHasChildren') && data['nodeHasChildren'] !== null) {\\n return data['nodeHasChildren'] === 'true';\\n} else {\\n return true;\\n}\\n \\n**/\\n \",\"nodeTextFunction\":\"/**\\n\\n// Function should return text (can be HTML code) for the current node.\\n\\n// The following example code will generate node text consisting of entity name and temperature if temperature value is present in entity attributes/timeseries.\\n\\nvar data = nodeCtx.data;\\nvar entity = nodeCtx.entity;\\nvar text = entity.name;\\nif (data.hasOwnProperty('temperature') && data['temperature'] !== null) {\\n text += \\\" \\\"+ data['temperature'] +\\\" °C\\\";\\n}\\nreturn text;\\n\\n**/\",\"nodeIconFunction\":\"/** \\n\\n// Function should return node icon info object.\\n// Resulting object should contain either 'materialIcon' or 'iconUrl' property. \\n// Where:\\n - 'materialIcon' - name of the material icon to be used from the Material Icons Library (https://material.io/tools/icons);\\n - 'iconUrl' - url of the external image to be used as node icon.\\n// Function can return 'default' string value. In this case default icons according to entity type will be used.\\n\\n// The following example code shows how to use external image for devices which name starts with 'Test' and use \\n// default icons for the rest of entities.\\n\\nvar entity = nodeCtx.entity;\\nif (entity.id.entityType === 'DEVICE' && entity.name.startsWith('Test')) {\\n return {iconUrl: 'https://avatars1.githubusercontent.com/u/14793288?v=4&s=117'};\\n} else {\\n return 'default';\\n}\\n \\n**/\",\"nodeDisabledFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be disabled (not selectable).\\n\\n// The following example code will disable current node according to the value of example 'nodeDisabled' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeDisabled') && data['nodeDisabled'] !== null) {\\n return data['nodeDisabled'] === 'true';\\n} else {\\n return false;\\n}\\n \\n**/\\n\",\"nodesSortFunction\":\"/**\\n\\n// This function is used to sort nodes of the same level. Function should compare two nodes and return \\n// integer value: \\n// - less than 0 - sort nodeCtx1 to an index lower than nodeCtx2\\n// - 0 - leave nodeCtx1 and nodeCtx2 unchanged with respect to each other\\n// - greater than 0 - sort nodeCtx2 to an index lower than nodeCtx1\\n\\n// The following example code will sort entities first by entity type in alphabetical order then\\n// by entity name in alphabetical order.\\n\\nvar result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);\\nif (result === 0) {\\n result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);\\n}\\nreturn result;\\n \\n**/\",\"nodeOpenedFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be opened (expanded) when it first loaded.\\n\\n// The following example code will open by default nodes up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n**/\\n \"},\"title\":\"Edges 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/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js index 56e4378eba..091c1287ba 100644 --- a/ui/src/app/api/entity.service.js +++ b/ui/src/app/api/entity.service.js @@ -42,7 +42,8 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device getRelatedEntity: getRelatedEntity, deleteRelatedEntity: deleteRelatedEntity, moveEntity: moveEntity, - copyEntity: copyEntity + copyEntity: copyEntity, + getAssignedToEdgeEntitiesByType: getAssignedToEdgeEntitiesByType }; return service; @@ -1684,4 +1685,26 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device } } + function getAssignedToEdgeEntitiesByType(edgeId, entityType, pageLink) { + var promise; + switch (entityType) { + case types.entityType.device: + promise = deviceService.getEdgeDevices(edgeId, pageLink, null); + break; + case types.entityType.asset: + promise = assetService.getEdgeAssets(edgeId, pageLink, null); + break; + case types.entityType.entityView: + promise = entityViewService.getEdgeEntityViews(edgeId, pageLink, null); + break; + case types.entityType.dashboard: + promise = dashboardService.getEdgeDashboards(edgeId, pageLink, null); + break; + case types.entityType.rulechain: + promise = ruleChainService.getEdgeRuleChains(edgeId, pageLink, null); + break; + } + return promise; + } + } diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index c9522365b6..e4aedb319f 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -856,7 +856,8 @@ "missing-related-rule-chains-title": "Edge has missing related rule chain(s)", "missing-related-rule-chains-text": "Assigned to edge rule chain(s) use rule nodes that forward message(s) to rule chain(s) that are not assigned to this edge.

List of missing rule chain(s):
{{missingRuleChains}}", "downlinks": "Downlinks", - "no-downlinks-prompt": "No downlinks found" + "no-downlinks-prompt": "No downlinks found", + "assigned-to-customer-widget": "Assigned to: {{customerTitle}}" }, "edge-event": { "type-dashboard": "Dashboard", diff --git a/ui/src/app/widget/lib/edges-overview-widget-copy.js b/ui/src/app/widget/lib/edges-overview-widget-copy.js new file mode 100644 index 0000000000..56921b264c --- /dev/null +++ b/ui/src/app/widget/lib/edges-overview-widget-copy.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-copy.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.edgesOverviewWidget2', []) +// .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-copy.scss b/ui/src/app/widget/lib/edges-overview-widget-copy.scss new file mode 100644 index 0000000000..59d335b2fa --- /dev/null +++ b/ui/src/app/widget/lib/edges-overview-widget-copy.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.js b/ui/src/app/widget/lib/edges-overview-widget.js index 278ccb52fb..396bc1a23e 100644 --- a/ui/src/app/widget/lib/edges-overview-widget.js +++ b/ui/src/app/widget/lib/edges-overview-widget.js @@ -16,7 +16,6 @@ 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 */ @@ -41,8 +40,7 @@ function EdgesOverviewWidget() { } /*@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*/) { +function EdgesOverviewWidgetController($element, $scope, $q, $timeout, $translate, toast, types, utils, entityService, edgeService, customerService, userService /*$filter, $mdMedia, $mdPanel, $document, $timeout, utils, types*/) { var vm = this; vm.showData = true; @@ -52,6 +50,7 @@ function EdgesOverviewWidgetController($element, $scope, $q, $timeout, toast, ty vm.nodeIdCounter = 0; vm.nodesMap = {}; + vm.entityNodesMap = {}; vm.entityGroupsNodesMap = {}; vm.pendingUpdateNodeTasks = {}; @@ -59,7 +58,18 @@ function EdgesOverviewWidgetController($element, $scope, $q, $timeout, toast, ty search: null }; - vm.edgeGroupsTypes = [ + vm.searchAction = { + name: 'action.search', + show: true, + onAction: function() { + vm.enterFilterMode(); + }, + icon: 'search' + }; + + vm.customerTitle = null; + + var edgeGroupsTypes = [ types.entityType.asset, types.entityType.device, types.entityType.entityView, @@ -69,6 +79,9 @@ function EdgesOverviewWidgetController($element, $scope, $q, $timeout, toast, ty vm.onNodesInserted = onNodesInserted; vm.onNodeSelected = onNodeSelected; + vm.enterFilterMode = enterFilterMode; + vm.exitFilterMode = exitFilterMode; + vm.searchCallback = searchCallback; $scope.$watch('vm.ctx', function() { if (vm.ctx && vm.ctx.defaultSubscription) { @@ -76,10 +89,17 @@ function EdgesOverviewWidgetController($element, $scope, $q, $timeout, toast, ty vm.widgetConfig = vm.ctx.widgetConfig; vm.subscription = vm.ctx.defaultSubscription; vm.datasources = vm.subscription.datasources; + initializeConfig(); updateDatasources(); } }); + $scope.$watch("vm.query.search", function(newVal, prevVal) { + if (!angular.equals(newVal, prevVal) && vm.query.search != null) { + updateSearchNodes(); + } + }); + $scope.$on('edges-overview-data-updated', function(event, hierarchyId) { if (vm.hierarchyId == hierarchyId) { if (vm.subscription) { @@ -88,10 +108,96 @@ function EdgesOverviewWidgetController($element, $scope, $q, $timeout, toast, ty } }); + function initializeConfig() { + + vm.ctx.widgetActions = [ vm.searchAction ]; + + var testNodeCtx = { + entity: { + id: { + entityType: 'DEVICE', + id: '123' + }, + name: 'TEST DEV1' + }, + data: {}, + level: 2 + }; + var parentNodeCtx = angular.copy(testNodeCtx); + parentNodeCtx.level = 1; + testNodeCtx.parentNodeCtx = parentNodeCtx; + + var nodeRelationQueryFunction = loadNodeCtxFunction(vm.settings.nodeRelationQueryFunction, 'nodeCtx', testNodeCtx); + var nodeIconFunction = loadNodeCtxFunction(vm.settings.nodeIconFunction, 'nodeCtx', testNodeCtx); + var nodeTextFunction = loadNodeCtxFunction(vm.settings.nodeTextFunction, 'nodeCtx', testNodeCtx); + var nodeDisabledFunction = loadNodeCtxFunction(vm.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx); + var nodeOpenedFunction = loadNodeCtxFunction(vm.settings.nodeOpenedFunction, 'nodeCtx', testNodeCtx); + var nodeHasChildrenFunction = loadNodeCtxFunction(vm.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx); + + var testNodeCtx2 = angular.copy(testNodeCtx); + testNodeCtx2.entity.name = 'TEST DEV2'; + + var nodesSortFunction = loadNodeCtxFunction(vm.settings.nodesSortFunction, 'nodeCtx1,nodeCtx2', testNodeCtx, testNodeCtx2); + + vm.nodeRelationQueryFunction = nodeRelationQueryFunction || defaultNodeRelationQueryFunction; + vm.nodeIconFunction = nodeIconFunction || defaultNodeIconFunction; + vm.nodeTextFunction = nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name); + vm.nodeDisabledFunction = nodeDisabledFunction || (() => false); + vm.nodeOpenedFunction = nodeOpenedFunction || defaultNodeOpenedFunction; + vm.nodeHasChildrenFunction = nodeHasChildrenFunction || (() => true); + vm.nodesSortFunction = nodesSortFunction || defaultSortFunction; + } + + function loadNodeCtxFunction(functionBody, argNames, ...args) { + var nodeCtxFunction = null; + if (angular.isDefined(functionBody) && functionBody.length) { + try { + nodeCtxFunction = new Function(argNames, functionBody); + var res = nodeCtxFunction.apply(null, args); + if (angular.isUndefined(res)) { + nodeCtxFunction = null; + } + } catch (e) { + nodeCtxFunction = null; + } + } + return nodeCtxFunction; + } + + function enterFilterMode () { + vm.query.search = ''; + vm.ctx.hideTitlePanel = true; + $timeout(()=>{ + angular.element(vm.ctx.$container).find('.searchInput').focus(); + }) + } + + function exitFilterMode () { + vm.query.search = null; + updateSearchNodes(); + vm.ctx.hideTitlePanel = false; + } + + function searchCallback (searchText, node) { + var theNode = vm.nodesMap[node.id]; + if (theNode && theNode.data.searchText) { + return theNode.data.searchText.includes(searchText.toLowerCase()); + } + return false; + } + function updateDatasources() { vm.loadNodes = loadNodes; } + function updateSearchNodes() { + if (vm.query.search != null) { + vm.nodeEditCallbacks.search(vm.query.search); + } else { + vm.nodeEditCallbacks.clearSearch(); + } + } + function onNodesInserted(nodes/*, parent*/) { if (nodes) { nodes.forEach((nodeId) => { @@ -185,64 +291,109 @@ function EdgesOverviewWidgetController($element, $scope, $q, $timeout, toast, ty 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) { + if (datasource.type === types.datasourceType.entity && datasource.entity.id.entityType === types.entityType.edge) { var selectedEdge = datasource.entity; + getCustomerTitle(selectedEdge.id.id); vm.ctx.widgetTitle = selectedEdge.name; - cb(loadNodesForEdge(selectedEdge.id.id, selectedEdge)) + cb(loadNodesForEdge(selectedEdge.id.id, selectedEdge)); + } else if (datasource.type === types.datasourceType.function) { + cb(loadNodesForEdge(null, null)); + } else { + vm.edgeIsDatasource = false; + cb([]); } + } else if (node.data && node.data.entity && node.data.entity.id.entityType === types.entityType.edge) { + var edgeId = node.data.entity.id.id; + var entityType = node.data.entityType; + var pageLink = {limit: 100}; + entityService.getAssignedToEdgeEntitiesByType(edgeId, entityType, pageLink).then( + (entities) => { + if (entities.data.length > 0) { + cb(entitiesToNodes(node.id, entities.data)); + } else { + cb([]); + } + } + ) } else { - cb([]); + cb([]); + } + } + + function entitiesToNodes(parentNodeId, entities) { + var nodes = []; + vm.entityNodesMap[parentNodeId] = {}; + if (entities) { + entities.forEach( + (entity) => { + var node = createEntityNode(parentNodeId, entity, entity.id.entityType); + nodes.push(node); + } + ); } + return nodes; + } + + function createEntityNode(parentNodeId, entity, entityType) { + var nodesMap = vm.entityNodesMap[parentNodeId]; + if (!nodesMap) { + nodesMap = {}; + vm.entityNodesMap[parentNodeId] = nodesMap; + } + var node = { + id: ++vm.nodeIdCounter, + icon: 'material-icons ' + iconForGroupType(entityType), + text: entity.name, + children: false, + data: { + entity, + internalId: entity.id.id + } + }; + nodesMap[entity.id.id] = node.id; + return node; } 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) + vm.entityGroupsNodesMap[parentNodeId] = {}; + var allowedGroupTypes = edgeGroupsTypes; + if (userService.getAuthority() === 'CUSTOMER_USER') { + allowedGroupTypes = 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; - }); + allowedGroupTypes.forEach( + (entityType) => { + var node = { + id: ++vm.nodeIdCounter, + icon: 'material-icons ' + iconForGroupType(entityType), + text: textForGroupType(entityType), + children: true, + data: { + entityType, + entity, + internalId: entity ? entity.id.id + '_' + entityType : utils.guid() + } + }; + nodes.push(node); + } + ) return nodes; } function iconForGroupType(groupType) { switch (groupType) { - case vm.types.entityType.asset: + case types.entityType.asset: return 'tb-asset-group'; - case vm.types.entityType.device: + case types.entityType.device: return 'tb-device-group'; - case vm.types.entityType.entityView: + case types.entityType.entityView: return 'tb-entity-view-group'; - case vm.types.entityType.dashboard: + case types.entityType.dashboard: return 'tb-dashboard-group'; - case vm.types.entityType.rulechain: + case types.entityType.rulechain: return 'tb-rule-chain-group'; } return ''; @@ -250,22 +401,35 @@ function EdgesOverviewWidgetController($element, $scope, $q, $timeout, toast, ty function textForGroupType(groupType) { switch (groupType) { - case vm.types.entityType.asset: + case types.entityType.asset: return $translate.instant('asset.assets'); - case vm.types.entityType.device: + case types.entityType.device: return $translate.instant('device.devices'); - case vm.types.entityType.entityView: + case types.entityType.entityView: return $translate.instant('entity-view.entity-views'); - case vm.types.entityType.rulechain: + case types.entityType.rulechain: return $translate.instant('rulechain.rulechains'); - case vm.types.entityType.dashboard: + case types.entityType.dashboard: return $translate.instant('dashboard.dashboards'); } return ''; } - function edgeGroupsNodeText(entityType) { + function getCustomerTitle(edgeId) { + edgeService.getEdge(edgeId, true).then( + function success(edge) { + customerService.getShortCustomerInfo(edge.customerId.id).then( + function success(customer) { + vm.customerTitle = $translate.instant('edge.assigned-to-customer-widget', {customerTitle: customer.title}); + } + ); + } + ) + } + function showError(errorText) { + var toastParent = angular.element('.tb-entities-hierarchy', $element); + toast.showError(errorText, toastParent, 'bottom left'); } function prepareNodes(nodes) { @@ -310,52 +474,7 @@ function EdgesOverviewWidgetController($element, $scope, $q, $timeout, toast, ty 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) { + function entityIdToNode(entityType, entityId, parentDatasource, parentNodeCtx) { var deferred = $q.defer(); var datasource = { dataKeys: parentDatasource.dataKeys, @@ -411,6 +530,35 @@ function EdgesOverviewWidgetController($element, $scope, $q, $timeout, toast, ty return deferred.promise; } + + function prepareNodeRelationQuery(nodeCtx) { + var relationQuery = vm.nodeRelationQueryFunction(nodeCtx); + if (relationQuery && relationQuery === 'default') { + relationQuery = defaultNodeRelationQueryFunction(nodeCtx); + } + return relationQuery; + } + + function defaultNodeRelationQueryFunction(nodeCtx) { + var entity = nodeCtx.entity; + var query = { + parameters: { + rootId: entity.id.id, + rootType: entity.id.entityType, + direction: types.entitySearchDirection.from, + relationTypeGroup: "COMMON", + maxLevel: 1 + }, + filters: [ + { + relationType: "Contains", + entityTypes: [] + } + ] + }; + return query; + } + function prepareNodeIcon(nodeCtx) { var iconInfo = vm.nodeIconFunction(nodeCtx); if (iconInfo && iconInfo === 'default') { @@ -470,9 +618,6 @@ function EdgesOverviewWidgetController($element, $scope, $q, $timeout, toast, ty case types.entityType.edge: materialIcon = 'router'; break; - case types.entityType.rulechain: - materialIcon = 'settings_ethernet'; - break; } } return { @@ -480,4 +625,15 @@ function EdgesOverviewWidgetController($element, $scope, $q, $timeout, toast, ty }; } + function defaultNodeOpenedFunction(nodeCtx) { + return nodeCtx.level <= 4; + } + + function defaultSortFunction(nodeCtx1, nodeCtx2) { + var result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); + if (result === 0) { + result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name); + } + return result; + } } diff --git a/ui/src/app/widget/lib/edges-overview-widget.scss b/ui/src/app/widget/lib/edges-overview-widget.scss index cd2d419ef2..fcb508fea1 100644 --- a/ui/src/app/widget/lib/edges-overview-widget.scss +++ b/ui/src/app/widget/lib/edges-overview-widget.scss @@ -19,6 +19,13 @@ min-height: 60px; max-height: 60px; } + + span { + padding: 15px; + font-size: 14px; + font-weight: 400; + opacity: .5; + } } } diff --git a/ui/src/app/widget/lib/edges-overview-widget.tpl.html b/ui/src/app/widget/lib/edges-overview-widget.tpl.html index feeefd1013..93a7349fbb 100644 --- a/ui/src/app/widget/lib/edges-overview-widget.tpl.html +++ b/ui/src/app/widget/lib/edges-overview-widget.tpl.html @@ -17,9 +17,35 @@ -->
+ {{ customerTitle }} + +
+ + search + + {{'entity.search' | translate}} + + + + + + + + close + + {{ 'action.close' | translate }} + + +
+