From cb6e4d13da7ece991596eaa89d2d57c572cde2d5 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Tue, 29 Dec 2020 10:41:49 +0200 Subject: [PATCH 01/17] Edge Widget --- .../json/system/widget_bundles/cards.json | 16 + ui-ngx/src/app/core/http/entity.service.ts | 24 + ui-ngx/src/app/core/http/widget.service.ts | 7 + ...e-instances-overview-widget.component.html | 51 ++ ...e-instances-overview-widget.component.scss | 121 ++++ ...dge-instances-overview-widget.component.ts | 554 ++++++++++++++++++ .../edge-instances-overview-widget.models.ts | 260 ++++++++ .../widget/widget-components.module.ts | 3 + .../dashboard-widget-select.component.ts | 9 +- 9 files changed, 1044 insertions(+), 1 deletion(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.models.ts diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 0d97afd5e7..cc49167184 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -132,6 +132,22 @@ "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: \\\"FROM\\\",\\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\":\"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\":{}}" } + }, + { + "alias": "edge_instances_overview", + "name": "Edge Instances Overview", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.edgeInstancesOverviewWidget.onDataUpdated();\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\": \"EdgeInstancesOverviewSettings\",\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: \\\"FROM\\\",\\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\":\"Edge Instances 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\":{}}" + } } ] } \ No newline at end of file diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 60d47cc4a5..95fad50c85 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -71,6 +71,7 @@ import { alarmFields } from '@shared/models/alarm.models'; import { EdgeService } from "@core/http/edge.service"; import { ruleChainType } from "@shared/models/rule-chain.models"; import { Router } from "@angular/router"; +import { NavTreeNode } from "@shared/components/nav-tree.component"; @Injectable({ providedIn: 'root' @@ -1157,4 +1158,27 @@ export class EntityService { datasource.dataKeys.push(dataKey); }); } + + public getAssignedToEdgeEntitiesByType(node: NavTreeNode, pageLink: PageLink): Observable> { + let edgeId = node.data.edge.id.id; + let entitiesObservable: Observable>; + switch (node.data.entityType) { + case (EntityType.ASSET): + entitiesObservable = this.assetService.getEdgeAssets(edgeId, pageLink, null); + break; + case (EntityType.DEVICE): + entitiesObservable = this.deviceService.getEdgeDevices(edgeId, pageLink, null); + break; + case (EntityType.ENTITY_VIEW): + entitiesObservable = this.entityViewService.getEdgeEntityViews(edgeId, pageLink, null); + break; + case (EntityType.DASHBOARD): + entitiesObservable = this.dashboardService.getEdgeDashboards(edgeId, pageLink, null); + break; + case (EntityType.RULE_CHAIN): + entitiesObservable = this.ruleChainService.getEdgeRuleChains(edgeId, pageLink, null); + break; + } + return entitiesObservable; + } } diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 4440d8a4a9..345cc6a7f2 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -30,6 +30,9 @@ import { filter, map, mergeMap, tap } from 'rxjs/operators'; import { WidgetTypeId } from '@shared/models/id/widget-type-id'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { ActivationEnd, Router } from '@angular/router'; +import { getCurrentAuthState } from "@core/auth/auth.selectors"; +import { Store } from "@ngrx/store"; +import { AppState } from "@core/core.state"; @Injectable({ providedIn: 'root' @@ -50,6 +53,7 @@ export class WidgetService { private utils: UtilsService, private resources: ResourcesService, private translate: TranslateService, + private store: Store, private router: Router ) { this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe( @@ -121,6 +125,9 @@ export class WidgetService { config?: RequestConfig): Observable> { return this.getBundleWidgetTypes(bundleAlias, isSystem, config).pipe( map((types) => { + if (!getCurrentAuthState(this.store).edgesSupportEnabled) { + types = types.filter(type => type.alias !== 'edges_hierarchy') + } types = types.sort((a, b) => { let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]); if (result === 0) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.html new file mode 100644 index 0000000000..59cabe5b7b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.html @@ -0,0 +1,51 @@ + +
+
+ +
+ + +   + + + +
+
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.scss new file mode 100644 index 0000000000..2ca5824dd0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.scss @@ -0,0 +1,121 @@ +/** + * 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. + */ +:host-context(.tb-has-timewindow) { + .tb-edge-instances-overview { + mat-toolbar { + height: 60px; + max-height: 60px; + .mat-toolbar-tools { + height: 60px; + max-height: 60px; + } + } + } +} + +:host { + .tb-edge-instances-overview { + mat-toolbar.mat-table-toolbar:not([color="primary"]) { + background: transparent; + } + mat-toolbar { + min-height: 39px; + max-height: 39px; + .mat-toolbar-tools { + min-height: 39px; + max-height: 39px; + } + } + + .tb-edges-nav-tree-panel { + overflow-x: auto; + overflow-y: auto; + } + } +} + +:host ::ng-deep { + .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; + } + + mat-icon.node-icon { + width: 22px; + min-width: 22px; + height: 22px; + min-height: 22px; + margin-right: 2px; + margin-bottom: 2px; + color: inherit; + vertical-align: middle; + + &.material-icons { + font-size: 18px; + line-height: 22px; + text-align: center; + } + } + + &.jstree-hovered:not(.jstree-clicked), + &.jstree-disabled { + div.node-icon { + opacity: .5; + } + } + } + } + } + + @media (max-width: 768px) { + .tb-nav-tree-container { + &.jstree-proton-responsive { + .jstree-anchor { + div.node-icon { + width: 40px; + height: 40px; + margin: 0; + background-size: 24px 24px; + } + + mat-icon.node-icon { + width: 40px; + min-width: 40px; + height: 40px; + min-height: 40px; + margin: 0; + + &.material-icons { + font-size: 24px; + line-height: 40px; + } + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.ts new file mode 100644 index 0000000000..2214ba301a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.ts @@ -0,0 +1,554 @@ +/// +/// 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 { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; +import { DatasourceData, DatasourceType, WidgetConfig, widgetType } from '@shared/models/widget.models'; +import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { UtilsService } from '@core/services/utils.service'; +import cssjs from '@core/css/css'; +import { fromEvent } from 'rxjs'; +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; +import { constructTableCssString } from '@home/components/widget/lib/table-widget.models'; +import { Overlay } from '@angular/cdk/overlay'; +import { + LoadNodesCallback, + NavTreeEditCallbacks, NodesCallback, + NodeSearchCallback, + NodeSelectedCallback, + NodesInsertedCallback +} from '@shared/components/nav-tree.component'; +import { EntityType } from '@shared/models/entity-type.models'; +import { deepClone, hashCode } from '@core/utils'; +import { + defaultNodeIconFunction, + defaultNodeOpenedFunction, + defaultNodeRelationQueryFunction, + defaultNodesSortFunction, + EdgeGroupsNodeData, + edgeGroupsNodeText, + edgeGroupsTypes, + EdgeNodeData, + edgeNodeText, + EdgeInstancesOverviewNode, + EdgeInstancesOverviewWidgetSettings, + HierarchyNavTreeNode, + HierarchyNodeContext, + HierarchyNodeDatasource, + iconUrlHtml, + loadNodeCtxFunction, + materialIconHtml, + NodeDisabledFunction, + NodeHasChildrenFunction, + NodeIconFunction, + NodeOpenedFunction, + NodeRelationQueryFunction, + NodesSortFunction, + NodeTextFunction +} from '@home/components/widget/lib/edge-instances-overview-widget.models'; +import { EdgeService } from "@core/http/edge.service"; +import { PageLink } from "@shared/models/page/page-link"; +import { Edge, EdgeInfo } from "@shared/models/edge.models"; +import { TranslateService } from "@ngx-translate/core"; +import { EntityService } from "@core/http/entity.service"; +import { Direction, SortOrder } from "@shared/models/page/sort-order"; +import { EntityRelationsQuery } from "@shared/models/relation.models"; +import { EntityFilter } from "@shared/models/query/query.models"; +import { AliasFilterType, RelationsQueryFilter } from "@shared/models/alias.models"; + +@Component({ + selector: 'tb-edge-instances-overview-widget', + templateUrl: './edge-instances-overview-widget.component.html', + styleUrls: ['./edge-instances-overview-widget.component.scss'] +}) +export class EdgeInstancesOverviewWidgetComponent extends PageComponent implements OnInit, AfterViewInit { + + @Input() + ctx: WidgetContext; + + @ViewChild('searchInput') searchInputField: ElementRef; + + public toastTargetId = 'edge-instances-overview-' + this.utils.guid(); + + public textSearchMode = false; + public textSearch = null; + + public nodeEditCallbacks: NavTreeEditCallbacks = {}; + + private settings: EdgeInstancesOverviewWidgetSettings; + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + private datasources: Array; + private data: Array>; + + private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {}; + private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {}; + private nodeIdCounter = 0; + + private nodeRelationQueryFunction: NodeRelationQueryFunction; + private nodeIconFunction: NodeIconFunction; + private nodeTextFunction: NodeTextFunction; + private nodeDisabledFunction: NodeDisabledFunction; + private nodeOpenedFunction: NodeOpenedFunction; + private nodeHasChildrenFunction: NodeHasChildrenFunction; + private nodesSortFunction: NodesSortFunction; + + private edgeNodesMap: {[parentNodeId: string]: {[edgeId: string]: string}} = {}; + private edgeGroupsNodesMap: {[edgeNodeId: string]: {[groupType: string]: string}} = {}; + + + private searchAction: WidgetAction = { + name: 'action.search', + show: false, + icon: 'search', + onAction: () => { + this.enterFilterMode(); + } + }; + + constructor(protected store: Store, + private elementRef: ElementRef, + private edgeService: EdgeService, + private entityService: EntityService, + private translateService: TranslateService, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private utils: UtilsService) { + super(store); + } + + ngOnInit(): void { + this.ctx.$scope.edgeInstancesOverviewWidget = this; + this.settings = this.ctx.settings; + this.widgetConfig = this.ctx.widgetConfig; + this.subscription = this.ctx.defaultSubscription; + this.datasources = this.subscription.datasources as Array; + this.data = this.subscription.dataPages[0].data; + this.initializeConfig(); + this.ctx.updateWidgetParams(); + } + + ngAfterViewInit(): void { + fromEvent(this.searchInputField.nativeElement, 'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(() => { + this.updateSearchNodes(); + }) + ) + .subscribe(); + } + + public onDataUpdated() { + this.updateNodeData(this.subscription.data); + } + + private initializeConfig() { + this.ctx.widgetActions = [this.searchAction]; + + const testNodeCtx: HierarchyNodeContext = { + entity: { + id: { + entityType: EntityType.DEVICE, + id: '123' + }, + name: 'TEST DEV1' + }, + data: {}, + level: 2 + }; + const parentNodeCtx = deepClone(testNodeCtx); + parentNodeCtx.level = 1; + testNodeCtx.parentNodeCtx = parentNodeCtx; + + this.nodeRelationQueryFunction = loadNodeCtxFunction(this.settings.nodeRelationQueryFunction, 'nodeCtx', testNodeCtx); + this.nodeIconFunction = loadNodeCtxFunction(this.settings.nodeIconFunction, 'nodeCtx', testNodeCtx); + this.nodeTextFunction = loadNodeCtxFunction(this.settings.nodeTextFunction, 'nodeCtx', testNodeCtx); + this.nodeDisabledFunction = loadNodeCtxFunction(this.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx); + this.nodeOpenedFunction = loadNodeCtxFunction(this.settings.nodeOpenedFunction, 'nodeCtx', testNodeCtx); + this.nodeHasChildrenFunction = loadNodeCtxFunction(this.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx); + + const testNodeCtx2 = deepClone(testNodeCtx); + testNodeCtx2.entity.name = 'TEST DEV2'; + + this.nodesSortFunction = loadNodeCtxFunction(this.settings.nodesSortFunction, 'nodeCtx1,nodeCtx2', testNodeCtx, testNodeCtx2); + + this.nodeRelationQueryFunction = this.nodeRelationQueryFunction || defaultNodeRelationQueryFunction; + this.nodeIconFunction = this.nodeIconFunction || defaultNodeIconFunction; + this.nodeTextFunction = this.nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name); + this.nodeDisabledFunction = this.nodeDisabledFunction || (() => false); + this.nodeOpenedFunction = this.nodeOpenedFunction || defaultNodeOpenedFunction; + this.nodeHasChildrenFunction = this.nodeHasChildrenFunction || (() => true); + this.nodesSortFunction = this.nodesSortFunction || defaultNodesSortFunction; + + const cssString = constructTableCssString(this.widgetConfig); + const cssParser = new cssjs(); + cssParser.testMode = false; + const namespace = 'edges-instances-overview-' + hashCode(cssString); + cssParser.cssPreviewNamespace = namespace; + cssParser.createStyleElement(namespace, cssString); + $(this.elementRef.nativeElement).addClass(namespace); + } + + private enterFilterMode() { + this.textSearchMode = true; + this.textSearch = ''; + this.ctx.hideTitlePanel = true; + this.ctx.detectChanges(true); + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.textSearchMode = false; + this.textSearch = null; + this.updateSearchNodes(); + this.ctx.hideTitlePanel = false; + this.ctx.detectChanges(true); + } + + private updateSearchNodes() { + if (this.textSearch != null) { + this.nodeEditCallbacks.search(this.textSearch); + } else { + this.nodeEditCallbacks.clearSearch(); + } + } + + private updateNodeData(subscriptionData: Array) { + const affectedNodes: string[] = []; + if (subscriptionData) { + subscriptionData.forEach((datasourceData) => { + const datasource = datasourceData.datasource as HierarchyNodeDatasource; + if (datasource.nodeId) { + const node = this.nodesMap[datasource.nodeId]; + const key = datasourceData.dataKey.label; + let value; + if (datasourceData.data && datasourceData.data.length) { + value = datasourceData.data[0][1]; + } + if (node.data.nodeCtx.data[key] !== value) { + if (affectedNodes.indexOf(datasource.nodeId) === -1) { + affectedNodes.push(datasource.nodeId); + } + node.data.nodeCtx.data[key] = value; + } + } + }); + } + affectedNodes.forEach((nodeId) => { + const node: HierarchyNavTreeNode = this.nodeEditCallbacks.getNode(nodeId); + if (node) { + this.updateNodeStyle(this.nodesMap[nodeId]); + } else { + this.pendingUpdateNodeTasks[nodeId] = () => { + this.updateNodeStyle(this.nodesMap[nodeId]); + }; + } + }); + } + + public loadNodes: LoadNodesCallback = (node, cb) => { + if (node.id === '#') { + const sortOrder: SortOrder = { property: 'name', direction: Direction.ASC }; + const pageLink = new PageLink(100, 0, null, sortOrder); + this.edgeService.getTenantEdgeInfos(pageLink).subscribe( + (edges) => { + cb(this.edgesToNodes(node.id, edges.data)) + }); + } else if (node.data.type === 'edge') { + const edge = node.data.entity; + cb(this.loadNodesForEdge(node.id, edge)); + } else if (node.data.type === 'edgeGroups') { + const pageLink = new PageLink(100); + this.entityService.getAssignedToEdgeEntitiesByType(node, pageLink).subscribe( + (entities) => { + if (entities.data.length > 0) { + cb(this.edgesToNodes(node.id, entities.data)); + } else { + cb([]); + } + } + ) + } + } + + private loadNodesForEdge(parentNodeId: string, edge: EdgeInfo): EdgeInstancesOverviewNode[] { + const nodes: EdgeInstancesOverviewNode[] = []; + const nodesMap = {}; + this.edgeGroupsNodesMap[parentNodeId] = nodesMap; + edgeGroupsTypes.forEach((entityType) => { + const node: EdgeInstancesOverviewNode = { + id: (++this.nodeIdCounter)+'', + icon: false, + text: edgeGroupsNodeText(this.translateService, entityType), + children: true, + data: { + type: 'edgeGroups', + entityType, + edge, + internalId: edge.id.id + '_' + entityType + } as EdgeGroupsNodeData + }; + nodes.push(node); + nodesMap[entityType] = node.id; + }); + return nodes; + } + + private createEdgeNode(parentNodeId: string, edge: Edge): EdgeInstancesOverviewNode { + let nodesMap = this.edgeNodesMap[parentNodeId]; + if (!nodesMap) { + nodesMap = {}; + this.edgeNodesMap[parentNodeId] = nodesMap; + } + const node: EdgeInstancesOverviewNode = { + id: (++this.nodeIdCounter)+'', + icon: false, + text: edgeNodeText(edge), + children: parentNodeId === '#', + state: { + disabled: false + }, + data: { + type: 'edge', + entity: edge, + internalId: edge.id.id + } as EdgeNodeData + }; + nodesMap[edge.id.id] = node.id; + return node; + } + + private edgesToNodes(parentNodeId: string, edges: Array): EdgeInstancesOverviewNode[] { + const nodes: EdgeInstancesOverviewNode[] = []; + this.edgeNodesMap[parentNodeId] = {}; + if (edges) { + edges.forEach((edge) => { + const node = this.createEdgeNode(parentNodeId, edge); + nodes.push(node); + }); + } + return nodes; + } + + public onNodeSelected: NodeSelectedCallback = (node, event) => { + let nodeId; + if (!node) { + nodeId = -1; + } else { + nodeId = node.id; + } + if (nodeId !== -1) { + const selectedNode = this.nodesMap[nodeId]; + if (selectedNode) { + const descriptors = this.ctx.actionsApi.getActionDescriptors('nodeSelected'); + if (descriptors.length) { + const entity = selectedNode.data.nodeCtx.entity; + this.ctx.actionsApi.handleWidgetAction(event, descriptors[0], entity.id, entity.name, { nodeCtx: selectedNode.data.nodeCtx }); + } + } + } + } + + public onNodesInserted: NodesInsertedCallback = (nodes) => { + if (nodes) { + nodes.forEach((nodeId) => { + const task = this.pendingUpdateNodeTasks[nodeId]; + if (task) { + task(); + delete this.pendingUpdateNodeTasks[nodeId]; + } + }); + } + } + + public searchCallback: NodeSearchCallback = (searchText, node) => { + const theNode = this.nodesMap[node.id]; + if (theNode && theNode.data.searchText) { + return theNode.data.searchText.includes(searchText.toLowerCase()); + } + return false; + } + + private updateNodeStyle(node: HierarchyNavTreeNode) { + const newText = this.prepareNodeText(node); + if (node.text !== newText) { + node.text = newText; + this.nodeEditCallbacks.updateNode(node.id, node.text); + } + const newDisabled = this.nodeDisabledFunction(node.data.nodeCtx); + if (node.state.disabled !== newDisabled) { + node.state.disabled = newDisabled; + if (node.state.disabled) { + this.nodeEditCallbacks.disableNode(node.id); + } else { + this.nodeEditCallbacks.enableNode(node.id); + } + } + const newHasChildren = this.nodeHasChildrenFunction(node.data.nodeCtx); + if (node.children !== newHasChildren) { + node.children = newHasChildren; + this.nodeEditCallbacks.setNodeHasChildren(node.id, node.children); + } + } + + private prepareNodes(nodes: HierarchyNavTreeNode[]): HierarchyNavTreeNode[] { + nodes = nodes.filter((node) => node !== null); + nodes.sort((node1, node2) => this.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx)); + return nodes; + } + + private prepareNodeText(node: HierarchyNavTreeNode): string { + const nodeIcon = this.prepareNodeIcon(node.data.nodeCtx); + const nodeText = this.nodeTextFunction(node.data.nodeCtx); + node.data.searchText = nodeText ? nodeText.replace(/<[^>]+>/g, '').toLowerCase() : ''; + return nodeIcon + nodeText; + } + + private prepareNodeIcon(nodeCtx: HierarchyNodeContext): string { + let iconInfo = this.nodeIconFunction(nodeCtx); + if (iconInfo) { + if (iconInfo === 'default') { + iconInfo = defaultNodeIconFunction(nodeCtx); + } + if (iconInfo && iconInfo !== 'default' && (iconInfo.iconUrl || iconInfo.materialIcon)) { + if (iconInfo.materialIcon) { + return materialIconHtml(iconInfo.materialIcon); + } else { + return iconUrlHtml(iconInfo.iconUrl); + } + } else { + return ''; + } + } else { + return ''; + } + } + + private datasourceToNode(datasource: HierarchyNodeDatasource, + data: DatasourceData[], + parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode { + const node: HierarchyNavTreeNode = { + id: (++this.nodeIdCounter) + '' + }; + this.nodesMap[node.id] = node; + datasource.nodeId = node.id; + node.icon = false; + const nodeCtx: HierarchyNodeContext = { + parentNodeCtx, + entity: { + id: { + id: datasource.entityId, + entityType: datasource.entityType + }, + name: datasource.entityName, + label: datasource.entityLabel ? datasource.entityLabel : datasource.entityName + }, + data: {} + }; + datasource.dataKeys.forEach((dataKey, index) => { + const keyData = data[index].data; + if (keyData && keyData.length && keyData[0].length > 1) { + nodeCtx.data[dataKey.label] = keyData[0][1]; + } else { + nodeCtx.data[dataKey.label] = ''; + } + }); + nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1; + node.data = { + datasource, + nodeCtx + }; + node.state = { + disabled: this.nodeDisabledFunction(node.data.nodeCtx), + opened: this.nodeOpenedFunction(node.data.nodeCtx) + }; + node.text = this.prepareNodeText(node); + node.children = this.nodeHasChildrenFunction(node.data.nodeCtx); + return node; + } + + private loadChildren(parentNode: HierarchyNavTreeNode, datasource: HierarchyNodeDatasource, childrenNodesLoadCb: NodesCallback) { + const nodeCtx = parentNode.data.nodeCtx; + nodeCtx.childrenNodesLoaded = false; + const entityFilter = this.prepareNodeRelationsQueryFilter(nodeCtx); + const childrenDatasource = { + dataKeys: datasource.dataKeys, + type: DatasourceType.entity, + filterId: datasource.filterId, + entityFilter + } as HierarchyNodeDatasource; + const subscriptionOptions: WidgetSubscriptionOptions = { + type: widgetType.latest, + datasources: [childrenDatasource], + callbacks: { + onSubscriptionMessage: (subscription, message) => { + this.ctx.showToast(message.severity, message.message, undefined, + 'bottom', 'left', this.toastTargetId); + }, + onInitialPageDataChanged: (subscription) => { + this.ctx.subscriptionApi.removeSubscription(subscription.id); + this.nodeEditCallbacks.refreshNode(parentNode.id); + }, + onDataUpdated: subscription => { + if (nodeCtx.childrenNodesLoaded) { + this.updateNodeData(subscription.data); + } else { + const datasourcesPageData = subscription.datasourcePages[0]; + const dataPageData = subscription.dataPages[0]; + const childNodes: HierarchyNavTreeNode[] = []; + datasourcesPageData.data.forEach((childDatasource, index) => { + childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, dataPageData.data[index])); + }); + nodeCtx.childrenNodesLoaded = true; + childrenNodesLoadCb(this.prepareNodes(childNodes)); + } + } + } + }; + this.ctx.subscriptionApi.createSubscription(subscriptionOptions, true); + } + + private prepareNodeRelationQuery(nodeCtx: HierarchyNodeContext): EntityRelationsQuery { + let relationQuery = this.nodeRelationQueryFunction(nodeCtx); + if (relationQuery && relationQuery === 'default') { + relationQuery = defaultNodeRelationQueryFunction(nodeCtx); + } + return relationQuery as EntityRelationsQuery; + } + + private prepareNodeRelationsQueryFilter(nodeCtx: HierarchyNodeContext): EntityFilter { + const relationQuery = this.prepareNodeRelationQuery(nodeCtx); + return { + rootEntity: { + id: relationQuery.parameters.rootId, + entityType: relationQuery.parameters.rootType + }, + direction: relationQuery.parameters.direction, + filters: relationQuery.filters, + maxLevel: relationQuery.parameters.maxLevel, + fetchLastLevelOnly: relationQuery.parameters.fetchLastLevelOnly, + type: AliasFilterType.relationsQuery + } as RelationsQueryFilter; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.models.ts new file mode 100644 index 0000000000..aa389c2bea --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.models.ts @@ -0,0 +1,260 @@ +/// +/// 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 { BaseData } from '@shared/models/base-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { NavTreeNode } from '@shared/components/nav-tree.component'; +import { Datasource } from '@shared/models/widget.models'; +import { isDefined, isUndefined } from '@core/utils'; +import { EntityRelationsQuery, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { Edge } from "@shared/models/edge.models"; +import { TranslateService } from "@ngx-translate/core"; + +export interface EdgeInstancesOverviewWidgetSettings { + nodeRelationQueryFunction: string; + nodeHasChildrenFunction: string; + nodeOpenedFunction: string; + nodeDisabledFunction: string; + nodeIconFunction: string; + nodeTextFunction: string; + nodesSortFunction: string; +} + +export interface HierarchyNodeContext { + parentNodeCtx?: HierarchyNodeContext; + entity: BaseData; + childrenNodesLoaded?: boolean; + level?: number; + data: {[key: string]: any}; +} + +export interface HierarchyNavTreeNode extends NavTreeNode { + data?: { + datasource: HierarchyNodeDatasource; + nodeCtx: HierarchyNodeContext; + searchText?: string; + }; +} + +export interface HierarchyNodeDatasource extends Datasource { + nodeId: string; +} + +export interface HierarchyNodeIconInfo { + iconUrl?: string; + materialIcon?: string; +} + +export type NodeRelationQueryFunction = (nodeCtx: HierarchyNodeContext) => EntityRelationsQuery | 'default'; +export type NodeTextFunction = (nodeCtx: HierarchyNodeContext) => string; +export type NodeDisabledFunction = (nodeCtx: HierarchyNodeContext) => boolean; +export type NodeIconFunction = (nodeCtx: HierarchyNodeContext) => HierarchyNodeIconInfo | 'default'; +export type NodeOpenedFunction = (nodeCtx: HierarchyNodeContext) => boolean; +export type NodeHasChildrenFunction = (nodeCtx: HierarchyNodeContext) => boolean; +export type NodesSortFunction = (nodeCtx1: HierarchyNodeContext, nodeCtx2: HierarchyNodeContext) => number; + +export function loadNodeCtxFunction any>(functionBody: string, argNames: string, ...args: any[]): F { + let nodeCtxFunction: F = null; + if (isDefined(functionBody) && functionBody.length) { + try { + nodeCtxFunction = new Function(argNames, functionBody) as F; + const res = nodeCtxFunction.apply(null, args); + if (isUndefined(res)) { + nodeCtxFunction = null; + } + } catch (e) { + nodeCtxFunction = null; + } + } + return nodeCtxFunction; +} + +export function materialIconHtml(materialIcon: string): string { + return '' + materialIcon + ''; +} + +export function iconUrlHtml(iconUrl: string): string { + return '
 
'; +} + +export function edgeGroupsNodeText(translate: TranslateService, entityType: EntityType): string { + const nodeIcon = materialIconByEntityType(entityType); + const nodeText = textForEdgeGroupsType(translate, entityType); + return nodeIcon + nodeText; +} + +export function edgeNodeText(edge: Edge): string { + const nodeIcon = materialIconByEntityType(edge.id.entityType); + const nodeText = edge.name; + return nodeIcon + nodeText; +} + +export function materialIconByEntityType(entityType: EntityType): string { + let materialIcon = 'insert_drive_file'; + switch (entityType) { + case EntityType.DEVICE: + materialIcon = 'devices_other'; + break; + case EntityType.ASSET: + materialIcon = 'domain'; + break; + case EntityType.CUSTOMER: + materialIcon = 'supervisor_account'; + break; + case EntityType.USER: + materialIcon = 'account_circle'; + break; + case EntityType.DASHBOARD: + materialIcon = 'dashboards'; + break; + case EntityType.ENTITY_VIEW: + materialIcon = 'view_quilt'; + break; + case EntityType.RULE_CHAIN: + materialIcon = 'settings_ethernet'; + break; + case EntityType.EDGE: + materialIcon = 'router'; + break; + } + return '' + materialIcon + ''; +} + +export function textForEdgeGroupsType(translate: TranslateService, entityType: EntityType): string { + let textForEdgeGroupsType: string = ''; + switch (entityType) { + case EntityType.DEVICE: + textForEdgeGroupsType = 'device.devices'; + break; + case EntityType.ASSET: + textForEdgeGroupsType = 'asset.assets'; + break; + case EntityType.DASHBOARD: + textForEdgeGroupsType = 'dashboard.dashboards'; + break; + case EntityType.ENTITY_VIEW: + textForEdgeGroupsType = 'entity-view.entity-views'; + break; + case EntityType.RULE_CHAIN: + textForEdgeGroupsType = 'rulechain.rulechains'; + break; + } + return translate.instant(textForEdgeGroupsType); +} + +export const defaultNodeRelationQueryFunction: NodeRelationQueryFunction = nodeCtx => { + const entity = nodeCtx.entity; + const query: EntityRelationsQuery = { + parameters: { + rootId: entity.id.id, + rootType: entity.id.entityType as EntityType, + direction: EntitySearchDirection.FROM, + relationTypeGroup: RelationTypeGroup.COMMON, + maxLevel: 1 + }, + filters: [ + { + relationType: 'Contains', + entityTypes: [] + } + ] + }; + return query; +}; + +export const edgeGroupsTypes: EntityType[] = [ + EntityType.ASSET, + EntityType.DEVICE, + EntityType.ENTITY_VIEW, + EntityType.DASHBOARD, + EntityType.RULE_CHAIN +] + +export const defaultNodeIconFunction: NodeIconFunction = nodeCtx => { + let materialIcon = 'insert_drive_file'; + const entity = nodeCtx.entity; + if (entity && entity.id && entity.id.entityType) { + switch (entity.id.entityType as EntityType | string) { + case 'function': + materialIcon = 'functions'; + break; + case EntityType.DEVICE: + materialIcon = 'devices_other'; + break; + case EntityType.ASSET: + materialIcon = 'domain'; + break; + case EntityType.TENANT: + materialIcon = 'supervisor_account'; + break; + case EntityType.CUSTOMER: + materialIcon = 'supervisor_account'; + break; + case EntityType.USER: + materialIcon = 'account_circle'; + break; + case EntityType.DASHBOARD: + materialIcon = 'dashboards'; + break; + case EntityType.ALARM: + materialIcon = 'notifications_active'; + break; + case EntityType.ENTITY_VIEW: + materialIcon = 'view_quilt'; + break; + } + } + return { + materialIcon + }; +}; + +export const defaultNodeOpenedFunction: NodeOpenedFunction = nodeCtx => { + return nodeCtx.level <= 4; +}; + +export const defaultNodesSortFunction: NodesSortFunction = (nodeCtx1, nodeCtx2) => { + let result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); + if (result === 0) { + result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name); + } + return result; +}; + +export interface EdgeInstancesOverviewNode extends NavTreeNode { + data?: EdgeInstancesOverviewNodeData; +} + +export type EdgeInstancesOverviewNodeData = EdgeGroupsNodeData | EdgeNodeData; + +export interface EdgeGroupsNodeData extends BaseEdgeInstancesOverviewNodeData { + type: 'edgeGroups'; + entityType: EntityType; + edge: Edge; +} + +export interface EdgeNodeData extends BaseEdgeInstancesOverviewNodeData { + type: 'edge'; + entity: Edge; +} + +export interface BaseEdgeInstancesOverviewNodeData { + type: EdgeInstancesOverviewNodeType; + internalId: string; +} + +export type EdgeInstancesOverviewNodeType = 'edge' | 'edgeGroups'; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index c2a158fcdf..70f6332417 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -35,6 +35,7 @@ import { TripAnimationComponent } from './trip-animation/trip-animation.componen import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component'; import { GatewayFormComponent } from './lib/gateway/gateway-form.component'; import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { EdgeInstancesOverviewWidgetComponent } from "@home/components/widget/lib/edge-instances-overview-widget.component"; @NgModule({ declarations: @@ -45,6 +46,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor AlarmsTableWidgetComponent, TimeseriesTableWidgetComponent, EntitiesHierarchyWidgetComponent, + EdgeInstancesOverviewWidgetComponent, DateRangeNavigatorWidgetComponent, DateRangeNavigatorPanelComponent, MultipleInputWidgetComponent, @@ -63,6 +65,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor AlarmsTableWidgetComponent, TimeseriesTableWidgetComponent, EntitiesHierarchyWidgetComponent, + EdgeInstancesOverviewWidgetComponent, RpcWidgetsModule, DateRangeNavigatorWidgetComponent, MultipleInputWidgetComponent, diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts index 0c90a9bdda..982ac584ed 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts @@ -22,6 +22,9 @@ import { WidgetService } from '@core/http/widget.service'; import { Widget, widgetType } from '@shared/models/widget.models'; import { toWidgetInfo } from '@home/models/widget-component.models'; import { DashboardCallbacks } from '../../models/dashboard-component.models'; +import { getCurrentAuthState } from "@core/auth/auth.selectors"; +import { Store } from "@ngrx/store"; +import { AppState } from "@core/core.state"; @Component({ selector: 'tb-dashboard-widget-select', @@ -49,7 +52,8 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { onWidgetClicked: this.onWidgetClicked.bind(this) }; - constructor(private widgetsService: WidgetService) { + constructor(private widgetsService: WidgetService, + private store: Store) { } ngOnInit(): void { @@ -112,6 +116,9 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { this.staticWidgetTypes.push(widget); break; } + if (!getCurrentAuthState(this.store).edgesSupportEnabled) { + this.latestWidgetTypes = this.latestWidgetTypes.filter(type => type.typeAlias !== 'edges_instances_overview') + } top += widget.sizeY; }); } From 58c580ecafe96decaeb2db5280aee94f569e158d Mon Sep 17 00:00:00 2001 From: deaflynx Date: Tue, 29 Dec 2020 14:44:18 +0200 Subject: [PATCH 02/17] Edge widgets with empty datasource --- .../json/system/widget_bundles/cards.json | 16 - .../DefaultSystemDataLoaderService.java | 1 + ui-ngx/src/app/core/http/widget.service.ts | 2 +- ...e-instances-overview-widget.component.html | 51 -- ...dge-instances-overview-widget.component.ts | 554 ------------------ .../edge-instances-overview-widget.models.ts | 260 -------- ...s => edges-overview-widget.component.scss} | 6 +- .../widget/widget-components.module.ts | 6 +- .../dashboard-widget-select.component.ts | 2 +- 9 files changed, 9 insertions(+), 889 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.html delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.ts delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.models.ts rename ui-ngx/src/app/modules/home/components/widget/lib/{edge-instances-overview-widget.component.scss => edges-overview-widget.component.scss} (96%) diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index cc49167184..0d97afd5e7 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -132,22 +132,6 @@ "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: \\\"FROM\\\",\\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\":\"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\":{}}" } - }, - { - "alias": "edge_instances_overview", - "name": "Edge Instances Overview", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 3.5, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.edgeInstancesOverviewWidget.onDataUpdated();\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\": \"EdgeInstancesOverviewSettings\",\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: \\\"FROM\\\",\\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\":\"Edge Instances 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\":{}}" - } } ] } \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index fe1b06399c..70acadd3e8 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -439,6 +439,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { this.deleteSystemWidgetBundle("input_widgets"); this.deleteSystemWidgetBundle("date"); this.deleteSystemWidgetBundle("entity_admin_widgets"); + this.deleteSystemWidgetBundle("edge_widgets"); installScripts.loadSystemWidgets(); } diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 345cc6a7f2..4bf35ca011 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -126,7 +126,7 @@ export class WidgetService { return this.getBundleWidgetTypes(bundleAlias, isSystem, config).pipe( map((types) => { if (!getCurrentAuthState(this.store).edgesSupportEnabled) { - types = types.filter(type => type.alias !== 'edges_hierarchy') + types = types.filter(type => type.alias !== 'edges_overview') } types = types.sort((a, b) => { let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.html deleted file mode 100644 index 59cabe5b7b..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.html +++ /dev/null @@ -1,51 +0,0 @@ - -
-
- -
- - -   - - - -
-
-
- -
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.ts deleted file mode 100644 index 2214ba301a..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.ts +++ /dev/null @@ -1,554 +0,0 @@ -/// -/// 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 { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; -import { PageComponent } from '@shared/components/page.component'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; -import { DatasourceData, DatasourceType, WidgetConfig, widgetType } from '@shared/models/widget.models'; -import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; -import { UtilsService } from '@core/services/utils.service'; -import cssjs from '@core/css/css'; -import { fromEvent } from 'rxjs'; -import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; -import { constructTableCssString } from '@home/components/widget/lib/table-widget.models'; -import { Overlay } from '@angular/cdk/overlay'; -import { - LoadNodesCallback, - NavTreeEditCallbacks, NodesCallback, - NodeSearchCallback, - NodeSelectedCallback, - NodesInsertedCallback -} from '@shared/components/nav-tree.component'; -import { EntityType } from '@shared/models/entity-type.models'; -import { deepClone, hashCode } from '@core/utils'; -import { - defaultNodeIconFunction, - defaultNodeOpenedFunction, - defaultNodeRelationQueryFunction, - defaultNodesSortFunction, - EdgeGroupsNodeData, - edgeGroupsNodeText, - edgeGroupsTypes, - EdgeNodeData, - edgeNodeText, - EdgeInstancesOverviewNode, - EdgeInstancesOverviewWidgetSettings, - HierarchyNavTreeNode, - HierarchyNodeContext, - HierarchyNodeDatasource, - iconUrlHtml, - loadNodeCtxFunction, - materialIconHtml, - NodeDisabledFunction, - NodeHasChildrenFunction, - NodeIconFunction, - NodeOpenedFunction, - NodeRelationQueryFunction, - NodesSortFunction, - NodeTextFunction -} from '@home/components/widget/lib/edge-instances-overview-widget.models'; -import { EdgeService } from "@core/http/edge.service"; -import { PageLink } from "@shared/models/page/page-link"; -import { Edge, EdgeInfo } from "@shared/models/edge.models"; -import { TranslateService } from "@ngx-translate/core"; -import { EntityService } from "@core/http/entity.service"; -import { Direction, SortOrder } from "@shared/models/page/sort-order"; -import { EntityRelationsQuery } from "@shared/models/relation.models"; -import { EntityFilter } from "@shared/models/query/query.models"; -import { AliasFilterType, RelationsQueryFilter } from "@shared/models/alias.models"; - -@Component({ - selector: 'tb-edge-instances-overview-widget', - templateUrl: './edge-instances-overview-widget.component.html', - styleUrls: ['./edge-instances-overview-widget.component.scss'] -}) -export class EdgeInstancesOverviewWidgetComponent extends PageComponent implements OnInit, AfterViewInit { - - @Input() - ctx: WidgetContext; - - @ViewChild('searchInput') searchInputField: ElementRef; - - public toastTargetId = 'edge-instances-overview-' + this.utils.guid(); - - public textSearchMode = false; - public textSearch = null; - - public nodeEditCallbacks: NavTreeEditCallbacks = {}; - - private settings: EdgeInstancesOverviewWidgetSettings; - private widgetConfig: WidgetConfig; - private subscription: IWidgetSubscription; - private datasources: Array; - private data: Array>; - - private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {}; - private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {}; - private nodeIdCounter = 0; - - private nodeRelationQueryFunction: NodeRelationQueryFunction; - private nodeIconFunction: NodeIconFunction; - private nodeTextFunction: NodeTextFunction; - private nodeDisabledFunction: NodeDisabledFunction; - private nodeOpenedFunction: NodeOpenedFunction; - private nodeHasChildrenFunction: NodeHasChildrenFunction; - private nodesSortFunction: NodesSortFunction; - - private edgeNodesMap: {[parentNodeId: string]: {[edgeId: string]: string}} = {}; - private edgeGroupsNodesMap: {[edgeNodeId: string]: {[groupType: string]: string}} = {}; - - - private searchAction: WidgetAction = { - name: 'action.search', - show: false, - icon: 'search', - onAction: () => { - this.enterFilterMode(); - } - }; - - constructor(protected store: Store, - private elementRef: ElementRef, - private edgeService: EdgeService, - private entityService: EntityService, - private translateService: TranslateService, - private overlay: Overlay, - private viewContainerRef: ViewContainerRef, - private utils: UtilsService) { - super(store); - } - - ngOnInit(): void { - this.ctx.$scope.edgeInstancesOverviewWidget = this; - this.settings = this.ctx.settings; - this.widgetConfig = this.ctx.widgetConfig; - this.subscription = this.ctx.defaultSubscription; - this.datasources = this.subscription.datasources as Array; - this.data = this.subscription.dataPages[0].data; - this.initializeConfig(); - this.ctx.updateWidgetParams(); - } - - ngAfterViewInit(): void { - fromEvent(this.searchInputField.nativeElement, 'keyup') - .pipe( - debounceTime(150), - distinctUntilChanged(), - tap(() => { - this.updateSearchNodes(); - }) - ) - .subscribe(); - } - - public onDataUpdated() { - this.updateNodeData(this.subscription.data); - } - - private initializeConfig() { - this.ctx.widgetActions = [this.searchAction]; - - const testNodeCtx: HierarchyNodeContext = { - entity: { - id: { - entityType: EntityType.DEVICE, - id: '123' - }, - name: 'TEST DEV1' - }, - data: {}, - level: 2 - }; - const parentNodeCtx = deepClone(testNodeCtx); - parentNodeCtx.level = 1; - testNodeCtx.parentNodeCtx = parentNodeCtx; - - this.nodeRelationQueryFunction = loadNodeCtxFunction(this.settings.nodeRelationQueryFunction, 'nodeCtx', testNodeCtx); - this.nodeIconFunction = loadNodeCtxFunction(this.settings.nodeIconFunction, 'nodeCtx', testNodeCtx); - this.nodeTextFunction = loadNodeCtxFunction(this.settings.nodeTextFunction, 'nodeCtx', testNodeCtx); - this.nodeDisabledFunction = loadNodeCtxFunction(this.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx); - this.nodeOpenedFunction = loadNodeCtxFunction(this.settings.nodeOpenedFunction, 'nodeCtx', testNodeCtx); - this.nodeHasChildrenFunction = loadNodeCtxFunction(this.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx); - - const testNodeCtx2 = deepClone(testNodeCtx); - testNodeCtx2.entity.name = 'TEST DEV2'; - - this.nodesSortFunction = loadNodeCtxFunction(this.settings.nodesSortFunction, 'nodeCtx1,nodeCtx2', testNodeCtx, testNodeCtx2); - - this.nodeRelationQueryFunction = this.nodeRelationQueryFunction || defaultNodeRelationQueryFunction; - this.nodeIconFunction = this.nodeIconFunction || defaultNodeIconFunction; - this.nodeTextFunction = this.nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name); - this.nodeDisabledFunction = this.nodeDisabledFunction || (() => false); - this.nodeOpenedFunction = this.nodeOpenedFunction || defaultNodeOpenedFunction; - this.nodeHasChildrenFunction = this.nodeHasChildrenFunction || (() => true); - this.nodesSortFunction = this.nodesSortFunction || defaultNodesSortFunction; - - const cssString = constructTableCssString(this.widgetConfig); - const cssParser = new cssjs(); - cssParser.testMode = false; - const namespace = 'edges-instances-overview-' + hashCode(cssString); - cssParser.cssPreviewNamespace = namespace; - cssParser.createStyleElement(namespace, cssString); - $(this.elementRef.nativeElement).addClass(namespace); - } - - private enterFilterMode() { - this.textSearchMode = true; - this.textSearch = ''; - this.ctx.hideTitlePanel = true; - this.ctx.detectChanges(true); - setTimeout(() => { - this.searchInputField.nativeElement.focus(); - this.searchInputField.nativeElement.setSelectionRange(0, 0); - }, 10); - } - - exitFilterMode() { - this.textSearchMode = false; - this.textSearch = null; - this.updateSearchNodes(); - this.ctx.hideTitlePanel = false; - this.ctx.detectChanges(true); - } - - private updateSearchNodes() { - if (this.textSearch != null) { - this.nodeEditCallbacks.search(this.textSearch); - } else { - this.nodeEditCallbacks.clearSearch(); - } - } - - private updateNodeData(subscriptionData: Array) { - const affectedNodes: string[] = []; - if (subscriptionData) { - subscriptionData.forEach((datasourceData) => { - const datasource = datasourceData.datasource as HierarchyNodeDatasource; - if (datasource.nodeId) { - const node = this.nodesMap[datasource.nodeId]; - const key = datasourceData.dataKey.label; - let value; - if (datasourceData.data && datasourceData.data.length) { - value = datasourceData.data[0][1]; - } - if (node.data.nodeCtx.data[key] !== value) { - if (affectedNodes.indexOf(datasource.nodeId) === -1) { - affectedNodes.push(datasource.nodeId); - } - node.data.nodeCtx.data[key] = value; - } - } - }); - } - affectedNodes.forEach((nodeId) => { - const node: HierarchyNavTreeNode = this.nodeEditCallbacks.getNode(nodeId); - if (node) { - this.updateNodeStyle(this.nodesMap[nodeId]); - } else { - this.pendingUpdateNodeTasks[nodeId] = () => { - this.updateNodeStyle(this.nodesMap[nodeId]); - }; - } - }); - } - - public loadNodes: LoadNodesCallback = (node, cb) => { - if (node.id === '#') { - const sortOrder: SortOrder = { property: 'name', direction: Direction.ASC }; - const pageLink = new PageLink(100, 0, null, sortOrder); - this.edgeService.getTenantEdgeInfos(pageLink).subscribe( - (edges) => { - cb(this.edgesToNodes(node.id, edges.data)) - }); - } else if (node.data.type === 'edge') { - const edge = node.data.entity; - cb(this.loadNodesForEdge(node.id, edge)); - } else if (node.data.type === 'edgeGroups') { - const pageLink = new PageLink(100); - this.entityService.getAssignedToEdgeEntitiesByType(node, pageLink).subscribe( - (entities) => { - if (entities.data.length > 0) { - cb(this.edgesToNodes(node.id, entities.data)); - } else { - cb([]); - } - } - ) - } - } - - private loadNodesForEdge(parentNodeId: string, edge: EdgeInfo): EdgeInstancesOverviewNode[] { - const nodes: EdgeInstancesOverviewNode[] = []; - const nodesMap = {}; - this.edgeGroupsNodesMap[parentNodeId] = nodesMap; - edgeGroupsTypes.forEach((entityType) => { - const node: EdgeInstancesOverviewNode = { - id: (++this.nodeIdCounter)+'', - icon: false, - text: edgeGroupsNodeText(this.translateService, entityType), - children: true, - data: { - type: 'edgeGroups', - entityType, - edge, - internalId: edge.id.id + '_' + entityType - } as EdgeGroupsNodeData - }; - nodes.push(node); - nodesMap[entityType] = node.id; - }); - return nodes; - } - - private createEdgeNode(parentNodeId: string, edge: Edge): EdgeInstancesOverviewNode { - let nodesMap = this.edgeNodesMap[parentNodeId]; - if (!nodesMap) { - nodesMap = {}; - this.edgeNodesMap[parentNodeId] = nodesMap; - } - const node: EdgeInstancesOverviewNode = { - id: (++this.nodeIdCounter)+'', - icon: false, - text: edgeNodeText(edge), - children: parentNodeId === '#', - state: { - disabled: false - }, - data: { - type: 'edge', - entity: edge, - internalId: edge.id.id - } as EdgeNodeData - }; - nodesMap[edge.id.id] = node.id; - return node; - } - - private edgesToNodes(parentNodeId: string, edges: Array): EdgeInstancesOverviewNode[] { - const nodes: EdgeInstancesOverviewNode[] = []; - this.edgeNodesMap[parentNodeId] = {}; - if (edges) { - edges.forEach((edge) => { - const node = this.createEdgeNode(parentNodeId, edge); - nodes.push(node); - }); - } - return nodes; - } - - public onNodeSelected: NodeSelectedCallback = (node, event) => { - let nodeId; - if (!node) { - nodeId = -1; - } else { - nodeId = node.id; - } - if (nodeId !== -1) { - const selectedNode = this.nodesMap[nodeId]; - if (selectedNode) { - const descriptors = this.ctx.actionsApi.getActionDescriptors('nodeSelected'); - if (descriptors.length) { - const entity = selectedNode.data.nodeCtx.entity; - this.ctx.actionsApi.handleWidgetAction(event, descriptors[0], entity.id, entity.name, { nodeCtx: selectedNode.data.nodeCtx }); - } - } - } - } - - public onNodesInserted: NodesInsertedCallback = (nodes) => { - if (nodes) { - nodes.forEach((nodeId) => { - const task = this.pendingUpdateNodeTasks[nodeId]; - if (task) { - task(); - delete this.pendingUpdateNodeTasks[nodeId]; - } - }); - } - } - - public searchCallback: NodeSearchCallback = (searchText, node) => { - const theNode = this.nodesMap[node.id]; - if (theNode && theNode.data.searchText) { - return theNode.data.searchText.includes(searchText.toLowerCase()); - } - return false; - } - - private updateNodeStyle(node: HierarchyNavTreeNode) { - const newText = this.prepareNodeText(node); - if (node.text !== newText) { - node.text = newText; - this.nodeEditCallbacks.updateNode(node.id, node.text); - } - const newDisabled = this.nodeDisabledFunction(node.data.nodeCtx); - if (node.state.disabled !== newDisabled) { - node.state.disabled = newDisabled; - if (node.state.disabled) { - this.nodeEditCallbacks.disableNode(node.id); - } else { - this.nodeEditCallbacks.enableNode(node.id); - } - } - const newHasChildren = this.nodeHasChildrenFunction(node.data.nodeCtx); - if (node.children !== newHasChildren) { - node.children = newHasChildren; - this.nodeEditCallbacks.setNodeHasChildren(node.id, node.children); - } - } - - private prepareNodes(nodes: HierarchyNavTreeNode[]): HierarchyNavTreeNode[] { - nodes = nodes.filter((node) => node !== null); - nodes.sort((node1, node2) => this.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx)); - return nodes; - } - - private prepareNodeText(node: HierarchyNavTreeNode): string { - const nodeIcon = this.prepareNodeIcon(node.data.nodeCtx); - const nodeText = this.nodeTextFunction(node.data.nodeCtx); - node.data.searchText = nodeText ? nodeText.replace(/<[^>]+>/g, '').toLowerCase() : ''; - return nodeIcon + nodeText; - } - - private prepareNodeIcon(nodeCtx: HierarchyNodeContext): string { - let iconInfo = this.nodeIconFunction(nodeCtx); - if (iconInfo) { - if (iconInfo === 'default') { - iconInfo = defaultNodeIconFunction(nodeCtx); - } - if (iconInfo && iconInfo !== 'default' && (iconInfo.iconUrl || iconInfo.materialIcon)) { - if (iconInfo.materialIcon) { - return materialIconHtml(iconInfo.materialIcon); - } else { - return iconUrlHtml(iconInfo.iconUrl); - } - } else { - return ''; - } - } else { - return ''; - } - } - - private datasourceToNode(datasource: HierarchyNodeDatasource, - data: DatasourceData[], - parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode { - const node: HierarchyNavTreeNode = { - id: (++this.nodeIdCounter) + '' - }; - this.nodesMap[node.id] = node; - datasource.nodeId = node.id; - node.icon = false; - const nodeCtx: HierarchyNodeContext = { - parentNodeCtx, - entity: { - id: { - id: datasource.entityId, - entityType: datasource.entityType - }, - name: datasource.entityName, - label: datasource.entityLabel ? datasource.entityLabel : datasource.entityName - }, - data: {} - }; - datasource.dataKeys.forEach((dataKey, index) => { - const keyData = data[index].data; - if (keyData && keyData.length && keyData[0].length > 1) { - nodeCtx.data[dataKey.label] = keyData[0][1]; - } else { - nodeCtx.data[dataKey.label] = ''; - } - }); - nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1; - node.data = { - datasource, - nodeCtx - }; - node.state = { - disabled: this.nodeDisabledFunction(node.data.nodeCtx), - opened: this.nodeOpenedFunction(node.data.nodeCtx) - }; - node.text = this.prepareNodeText(node); - node.children = this.nodeHasChildrenFunction(node.data.nodeCtx); - return node; - } - - private loadChildren(parentNode: HierarchyNavTreeNode, datasource: HierarchyNodeDatasource, childrenNodesLoadCb: NodesCallback) { - const nodeCtx = parentNode.data.nodeCtx; - nodeCtx.childrenNodesLoaded = false; - const entityFilter = this.prepareNodeRelationsQueryFilter(nodeCtx); - const childrenDatasource = { - dataKeys: datasource.dataKeys, - type: DatasourceType.entity, - filterId: datasource.filterId, - entityFilter - } as HierarchyNodeDatasource; - const subscriptionOptions: WidgetSubscriptionOptions = { - type: widgetType.latest, - datasources: [childrenDatasource], - callbacks: { - onSubscriptionMessage: (subscription, message) => { - this.ctx.showToast(message.severity, message.message, undefined, - 'bottom', 'left', this.toastTargetId); - }, - onInitialPageDataChanged: (subscription) => { - this.ctx.subscriptionApi.removeSubscription(subscription.id); - this.nodeEditCallbacks.refreshNode(parentNode.id); - }, - onDataUpdated: subscription => { - if (nodeCtx.childrenNodesLoaded) { - this.updateNodeData(subscription.data); - } else { - const datasourcesPageData = subscription.datasourcePages[0]; - const dataPageData = subscription.dataPages[0]; - const childNodes: HierarchyNavTreeNode[] = []; - datasourcesPageData.data.forEach((childDatasource, index) => { - childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, dataPageData.data[index])); - }); - nodeCtx.childrenNodesLoaded = true; - childrenNodesLoadCb(this.prepareNodes(childNodes)); - } - } - } - }; - this.ctx.subscriptionApi.createSubscription(subscriptionOptions, true); - } - - private prepareNodeRelationQuery(nodeCtx: HierarchyNodeContext): EntityRelationsQuery { - let relationQuery = this.nodeRelationQueryFunction(nodeCtx); - if (relationQuery && relationQuery === 'default') { - relationQuery = defaultNodeRelationQueryFunction(nodeCtx); - } - return relationQuery as EntityRelationsQuery; - } - - private prepareNodeRelationsQueryFilter(nodeCtx: HierarchyNodeContext): EntityFilter { - const relationQuery = this.prepareNodeRelationQuery(nodeCtx); - return { - rootEntity: { - id: relationQuery.parameters.rootId, - entityType: relationQuery.parameters.rootType - }, - direction: relationQuery.parameters.direction, - filters: relationQuery.filters, - maxLevel: relationQuery.parameters.maxLevel, - fetchLastLevelOnly: relationQuery.parameters.fetchLastLevelOnly, - type: AliasFilterType.relationsQuery - } as RelationsQueryFilter; - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.models.ts deleted file mode 100644 index aa389c2bea..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.models.ts +++ /dev/null @@ -1,260 +0,0 @@ -/// -/// 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 { BaseData } from '@shared/models/base-data'; -import { EntityId } from '@shared/models/id/entity-id'; -import { NavTreeNode } from '@shared/components/nav-tree.component'; -import { Datasource } from '@shared/models/widget.models'; -import { isDefined, isUndefined } from '@core/utils'; -import { EntityRelationsQuery, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models'; -import { EntityType } from '@shared/models/entity-type.models'; -import { Edge } from "@shared/models/edge.models"; -import { TranslateService } from "@ngx-translate/core"; - -export interface EdgeInstancesOverviewWidgetSettings { - nodeRelationQueryFunction: string; - nodeHasChildrenFunction: string; - nodeOpenedFunction: string; - nodeDisabledFunction: string; - nodeIconFunction: string; - nodeTextFunction: string; - nodesSortFunction: string; -} - -export interface HierarchyNodeContext { - parentNodeCtx?: HierarchyNodeContext; - entity: BaseData; - childrenNodesLoaded?: boolean; - level?: number; - data: {[key: string]: any}; -} - -export interface HierarchyNavTreeNode extends NavTreeNode { - data?: { - datasource: HierarchyNodeDatasource; - nodeCtx: HierarchyNodeContext; - searchText?: string; - }; -} - -export interface HierarchyNodeDatasource extends Datasource { - nodeId: string; -} - -export interface HierarchyNodeIconInfo { - iconUrl?: string; - materialIcon?: string; -} - -export type NodeRelationQueryFunction = (nodeCtx: HierarchyNodeContext) => EntityRelationsQuery | 'default'; -export type NodeTextFunction = (nodeCtx: HierarchyNodeContext) => string; -export type NodeDisabledFunction = (nodeCtx: HierarchyNodeContext) => boolean; -export type NodeIconFunction = (nodeCtx: HierarchyNodeContext) => HierarchyNodeIconInfo | 'default'; -export type NodeOpenedFunction = (nodeCtx: HierarchyNodeContext) => boolean; -export type NodeHasChildrenFunction = (nodeCtx: HierarchyNodeContext) => boolean; -export type NodesSortFunction = (nodeCtx1: HierarchyNodeContext, nodeCtx2: HierarchyNodeContext) => number; - -export function loadNodeCtxFunction any>(functionBody: string, argNames: string, ...args: any[]): F { - let nodeCtxFunction: F = null; - if (isDefined(functionBody) && functionBody.length) { - try { - nodeCtxFunction = new Function(argNames, functionBody) as F; - const res = nodeCtxFunction.apply(null, args); - if (isUndefined(res)) { - nodeCtxFunction = null; - } - } catch (e) { - nodeCtxFunction = null; - } - } - return nodeCtxFunction; -} - -export function materialIconHtml(materialIcon: string): string { - return '' + materialIcon + ''; -} - -export function iconUrlHtml(iconUrl: string): string { - return '
 
'; -} - -export function edgeGroupsNodeText(translate: TranslateService, entityType: EntityType): string { - const nodeIcon = materialIconByEntityType(entityType); - const nodeText = textForEdgeGroupsType(translate, entityType); - return nodeIcon + nodeText; -} - -export function edgeNodeText(edge: Edge): string { - const nodeIcon = materialIconByEntityType(edge.id.entityType); - const nodeText = edge.name; - return nodeIcon + nodeText; -} - -export function materialIconByEntityType(entityType: EntityType): string { - let materialIcon = 'insert_drive_file'; - switch (entityType) { - case EntityType.DEVICE: - materialIcon = 'devices_other'; - break; - case EntityType.ASSET: - materialIcon = 'domain'; - break; - case EntityType.CUSTOMER: - materialIcon = 'supervisor_account'; - break; - case EntityType.USER: - materialIcon = 'account_circle'; - break; - case EntityType.DASHBOARD: - materialIcon = 'dashboards'; - break; - case EntityType.ENTITY_VIEW: - materialIcon = 'view_quilt'; - break; - case EntityType.RULE_CHAIN: - materialIcon = 'settings_ethernet'; - break; - case EntityType.EDGE: - materialIcon = 'router'; - break; - } - return '' + materialIcon + ''; -} - -export function textForEdgeGroupsType(translate: TranslateService, entityType: EntityType): string { - let textForEdgeGroupsType: string = ''; - switch (entityType) { - case EntityType.DEVICE: - textForEdgeGroupsType = 'device.devices'; - break; - case EntityType.ASSET: - textForEdgeGroupsType = 'asset.assets'; - break; - case EntityType.DASHBOARD: - textForEdgeGroupsType = 'dashboard.dashboards'; - break; - case EntityType.ENTITY_VIEW: - textForEdgeGroupsType = 'entity-view.entity-views'; - break; - case EntityType.RULE_CHAIN: - textForEdgeGroupsType = 'rulechain.rulechains'; - break; - } - return translate.instant(textForEdgeGroupsType); -} - -export const defaultNodeRelationQueryFunction: NodeRelationQueryFunction = nodeCtx => { - const entity = nodeCtx.entity; - const query: EntityRelationsQuery = { - parameters: { - rootId: entity.id.id, - rootType: entity.id.entityType as EntityType, - direction: EntitySearchDirection.FROM, - relationTypeGroup: RelationTypeGroup.COMMON, - maxLevel: 1 - }, - filters: [ - { - relationType: 'Contains', - entityTypes: [] - } - ] - }; - return query; -}; - -export const edgeGroupsTypes: EntityType[] = [ - EntityType.ASSET, - EntityType.DEVICE, - EntityType.ENTITY_VIEW, - EntityType.DASHBOARD, - EntityType.RULE_CHAIN -] - -export const defaultNodeIconFunction: NodeIconFunction = nodeCtx => { - let materialIcon = 'insert_drive_file'; - const entity = nodeCtx.entity; - if (entity && entity.id && entity.id.entityType) { - switch (entity.id.entityType as EntityType | string) { - case 'function': - materialIcon = 'functions'; - break; - case EntityType.DEVICE: - materialIcon = 'devices_other'; - break; - case EntityType.ASSET: - materialIcon = 'domain'; - break; - case EntityType.TENANT: - materialIcon = 'supervisor_account'; - break; - case EntityType.CUSTOMER: - materialIcon = 'supervisor_account'; - break; - case EntityType.USER: - materialIcon = 'account_circle'; - break; - case EntityType.DASHBOARD: - materialIcon = 'dashboards'; - break; - case EntityType.ALARM: - materialIcon = 'notifications_active'; - break; - case EntityType.ENTITY_VIEW: - materialIcon = 'view_quilt'; - break; - } - } - return { - materialIcon - }; -}; - -export const defaultNodeOpenedFunction: NodeOpenedFunction = nodeCtx => { - return nodeCtx.level <= 4; -}; - -export const defaultNodesSortFunction: NodesSortFunction = (nodeCtx1, nodeCtx2) => { - let result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); - if (result === 0) { - result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name); - } - return result; -}; - -export interface EdgeInstancesOverviewNode extends NavTreeNode { - data?: EdgeInstancesOverviewNodeData; -} - -export type EdgeInstancesOverviewNodeData = EdgeGroupsNodeData | EdgeNodeData; - -export interface EdgeGroupsNodeData extends BaseEdgeInstancesOverviewNodeData { - type: 'edgeGroups'; - entityType: EntityType; - edge: Edge; -} - -export interface EdgeNodeData extends BaseEdgeInstancesOverviewNodeData { - type: 'edge'; - entity: Edge; -} - -export interface BaseEdgeInstancesOverviewNodeData { - type: EdgeInstancesOverviewNodeType; - internalId: string; -} - -export type EdgeInstancesOverviewNodeType = 'edge' | 'edgeGroups'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.scss similarity index 96% rename from ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.scss index 2ca5824dd0..abd1536e98 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edge-instances-overview-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.scss @@ -14,7 +14,7 @@ * limitations under the License. */ :host-context(.tb-has-timewindow) { - .tb-edge-instances-overview { + .tb-edges-overview { mat-toolbar { height: 60px; max-height: 60px; @@ -27,7 +27,7 @@ } :host { - .tb-edge-instances-overview { + .tb-edges-overview { mat-toolbar.mat-table-toolbar:not([color="primary"]) { background: transparent; } @@ -40,7 +40,7 @@ } } - .tb-edges-nav-tree-panel { + .tb-entities-nav-tree-panel { overflow-x: auto; overflow-y: auto; } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 70f6332417..f2390d099a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -35,7 +35,7 @@ import { TripAnimationComponent } from './trip-animation/trip-animation.componen import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component'; import { GatewayFormComponent } from './lib/gateway/gateway-form.component'; import { ImportExportService } from '@home/components/import-export/import-export.service'; -import { EdgeInstancesOverviewWidgetComponent } from "@home/components/widget/lib/edge-instances-overview-widget.component"; +import { EdgesOverviewWidgetComponent } from "@home/components/widget/lib/edges-overview-widget.component"; @NgModule({ declarations: @@ -46,7 +46,7 @@ import { EdgeInstancesOverviewWidgetComponent } from "@home/components/widget/li AlarmsTableWidgetComponent, TimeseriesTableWidgetComponent, EntitiesHierarchyWidgetComponent, - EdgeInstancesOverviewWidgetComponent, + EdgesOverviewWidgetComponent, DateRangeNavigatorWidgetComponent, DateRangeNavigatorPanelComponent, MultipleInputWidgetComponent, @@ -65,7 +65,7 @@ import { EdgeInstancesOverviewWidgetComponent } from "@home/components/widget/li AlarmsTableWidgetComponent, TimeseriesTableWidgetComponent, EntitiesHierarchyWidgetComponent, - EdgeInstancesOverviewWidgetComponent, + EdgesOverviewWidgetComponent, RpcWidgetsModule, DateRangeNavigatorWidgetComponent, MultipleInputWidgetComponent, diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts index 982ac584ed..438bd905df 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts @@ -117,7 +117,7 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { break; } if (!getCurrentAuthState(this.store).edgesSupportEnabled) { - this.latestWidgetTypes = this.latestWidgetTypes.filter(type => type.typeAlias !== 'edges_instances_overview') + this.staticWidgetTypes = this.staticWidgetTypes.filter(type => type.typeAlias !== 'edges_instances_overview') } top += widget.sizeY; }); From 1b2763224411f95523e14a550aa0eb48671c72f7 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Tue, 29 Dec 2020 14:44:53 +0200 Subject: [PATCH 03/17] Edge widgets with empty datasource --- .../system/widget_bundles/edge_widgets.json | 25 + .../lib/edges-overview-widget.component.html | 51 ++ .../lib/edges-overview-widget.component.ts | 550 ++++++++++++++++++ .../lib/edges-overview-widget.models.ts | 260 +++++++++ 4 files changed, 886 insertions(+) create mode 100644 application/src/main/data/json/system/widget_bundles/edge_widgets.json create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts 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..5e66e11d55 --- /dev/null +++ b/application/src/main/data/json/system/widget_bundles/edge_widgets.json @@ -0,0 +1,25 @@ +{ + "widgetsBundle": { + "alias": "edges_widgets", + "title": "Edge Instances widgets", + "image": null + }, + "widgetTypes": [ + { + "alias": "edges_overview", + "name": "Edge Instances Overview", + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.edgesOverviewWidget.onDataUpdated();\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: \\\"FROM\\\",\\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\":\"Edge Instances 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\":{}}" + } + } + ] +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html new file mode 100644 index 0000000000..355b3538c9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html @@ -0,0 +1,51 @@ + +
+
+ +
+ + +   + + + +
+
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts new file mode 100644 index 0000000000..0666b840b8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts @@ -0,0 +1,550 @@ +/// +/// 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 { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; +import { DatasourceData, DatasourceType, WidgetConfig, widgetType } from '@shared/models/widget.models'; +import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { UtilsService } from '@core/services/utils.service'; +import cssjs from '@core/css/css'; +import { fromEvent } from 'rxjs'; +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; +import { constructTableCssString } from '@home/components/widget/lib/table-widget.models'; +import { Overlay } from '@angular/cdk/overlay'; +import { + LoadNodesCallback, + NavTreeEditCallbacks, + NodesCallback, + NodeSearchCallback, + NodeSelectedCallback, + NodesInsertedCallback +} from '@shared/components/nav-tree.component'; +import { EntityType } from '@shared/models/entity-type.models'; +import { deepClone, hashCode } from '@core/utils'; +import { + defaultNodeIconFunction, + defaultNodeOpenedFunction, + defaultNodeRelationQueryFunction, + defaultNodesSortFunction, + EdgeGroupsNodeData, + edgeGroupsNodeText, + edgeGroupsTypes, + EdgeNodeData, + edgeNodeText, + EdgeOverviewNode, EdgesOverviewWidgetSettings, + HierarchyNavTreeNode, + HierarchyNodeContext, + HierarchyNodeDatasource, + iconUrlHtml, + loadNodeCtxFunction, + materialIconHtml, + NodeDisabledFunction, + NodeHasChildrenFunction, + NodeIconFunction, + NodeOpenedFunction, + NodeRelationQueryFunction, + NodesSortFunction, + NodeTextFunction +} from '@home/components/widget/lib/edges-overview-widget.models'; +import { EntityRelationsQuery } from '@shared/models/relation.models'; +import { AliasFilterType, RelationsQueryFilter } from '@shared/models/alias.models'; +import { EntityFilter } from '@shared/models/query/query.models'; +import { EdgeService } from "@core/http/edge.service"; +import { EntityService } from "@core/http/entity.service"; +import { TranslateService } from "@ngx-translate/core"; +import { Direction, SortOrder } from "@shared/models/page/sort-order"; +import { PageLink } from "@shared/models/page/page-link"; +import { Edge, EdgeInfo } from "@shared/models/edge.models"; + +@Component({ + selector: 'tb-edges-overview-widget', + templateUrl: './edges-overview-widget.component.html', + styleUrls: ['./edges-overview-widget.component.scss'] +}) +export class EdgesOverviewWidgetComponent extends PageComponent implements OnInit, AfterViewInit { + + @Input() + ctx: WidgetContext; + + @ViewChild('searchInput') searchInputField: ElementRef; + + public toastTargetId = 'edges-overview-' + this.utils.guid(); + + public textSearchMode = false; + public textSearch = null; + + public nodeEditCallbacks: NavTreeEditCallbacks = {}; + + private settings: EdgesOverviewWidgetSettings; + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + + private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {}; + private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {}; + private nodeIdCounter = 0; + + private nodeRelationQueryFunction: NodeRelationQueryFunction; + private nodeIconFunction: NodeIconFunction; + private nodeTextFunction: NodeTextFunction; + private nodeDisabledFunction: NodeDisabledFunction; + private nodeOpenedFunction: NodeOpenedFunction; + private nodeHasChildrenFunction: NodeHasChildrenFunction; + private nodesSortFunction: NodesSortFunction; + + private edgeNodesMap: {[parentNodeId: string]: {[edgeId: string]: string}} = {}; + private edgeGroupsNodesMap: {[edgeNodeId: string]: {[groupType: string]: string}} = {}; + + + private searchAction: WidgetAction = { + name: 'action.search', + show: true, + icon: 'search', + onAction: () => { + this.enterFilterMode(); + } + }; + + constructor(protected store: Store, + private elementRef: ElementRef, + private edgeService: EdgeService, + private entityService: EntityService, + private translateService: TranslateService, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private utils: UtilsService) { + super(store); + } + + ngOnInit(): void { + this.ctx.$scope.edgesOverviewWidget = this; + this.settings = this.ctx.settings; + this.widgetConfig = this.ctx.widgetConfig; + this.subscription = this.ctx.defaultSubscription; + this.initializeConfig(); + this.ctx.updateWidgetParams(); + } + + ngAfterViewInit(): void { + fromEvent(this.searchInputField.nativeElement, 'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(() => { + this.updateSearchNodes(); + }) + ) + .subscribe(); + } + + public onDataUpdated() { + this.updateNodeData(this.subscription.data); + } + + private initializeConfig() { + this.ctx.widgetActions = [this.searchAction]; + + const testNodeCtx: HierarchyNodeContext = { + entity: { + id: { + entityType: EntityType.DEVICE, + id: '123' + }, + name: 'TEST DEV1' + }, + data: {}, + level: 2 + }; + const parentNodeCtx = deepClone(testNodeCtx); + parentNodeCtx.level = 1; + testNodeCtx.parentNodeCtx = parentNodeCtx; + + this.nodeRelationQueryFunction = loadNodeCtxFunction(this.settings.nodeRelationQueryFunction, 'nodeCtx', testNodeCtx); + this.nodeIconFunction = loadNodeCtxFunction(this.settings.nodeIconFunction, 'nodeCtx', testNodeCtx); + this.nodeTextFunction = loadNodeCtxFunction(this.settings.nodeTextFunction, 'nodeCtx', testNodeCtx); + this.nodeDisabledFunction = loadNodeCtxFunction(this.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx); + this.nodeOpenedFunction = loadNodeCtxFunction(this.settings.nodeOpenedFunction, 'nodeCtx', testNodeCtx); + this.nodeHasChildrenFunction = loadNodeCtxFunction(this.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx); + + const testNodeCtx2 = deepClone(testNodeCtx); + testNodeCtx2.entity.name = 'TEST DEV2'; + + this.nodesSortFunction = loadNodeCtxFunction(this.settings.nodesSortFunction, 'nodeCtx1,nodeCtx2', testNodeCtx, testNodeCtx2); + + this.nodeRelationQueryFunction = this.nodeRelationQueryFunction || defaultNodeRelationQueryFunction; + this.nodeIconFunction = this.nodeIconFunction || defaultNodeIconFunction; + this.nodeTextFunction = this.nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name); + this.nodeDisabledFunction = this.nodeDisabledFunction || (() => false); + this.nodeOpenedFunction = this.nodeOpenedFunction || defaultNodeOpenedFunction; + this.nodeHasChildrenFunction = this.nodeHasChildrenFunction || (() => true); + this.nodesSortFunction = this.nodesSortFunction || defaultNodesSortFunction; + + const cssString = constructTableCssString(this.widgetConfig); + const cssParser = new cssjs(); + cssParser.testMode = false; + const namespace = 'edges-overview-' + hashCode(cssString); + cssParser.cssPreviewNamespace = namespace; + cssParser.createStyleElement(namespace, cssString); + $(this.elementRef.nativeElement).addClass(namespace); + } + + private enterFilterMode() { + this.textSearchMode = true; + this.textSearch = ''; + this.ctx.hideTitlePanel = true; + this.ctx.detectChanges(true); + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.textSearchMode = false; + this.textSearch = null; + this.updateSearchNodes(); + this.ctx.hideTitlePanel = false; + this.ctx.detectChanges(true); + } + + private updateSearchNodes() { + if (this.textSearch != null) { + this.nodeEditCallbacks.search(this.textSearch); + } else { + this.nodeEditCallbacks.clearSearch(); + } + } + + private updateNodeData(subscriptionData: Array) { + const affectedNodes: string[] = []; + if (subscriptionData) { + subscriptionData.forEach((datasourceData) => { + const datasource = datasourceData.datasource as HierarchyNodeDatasource; + if (datasource.nodeId) { + const node = this.nodesMap[datasource.nodeId]; + const key = datasourceData.dataKey.label; + let value; + if (datasourceData.data && datasourceData.data.length) { + value = datasourceData.data[0][1]; + } + if (node.data.nodeCtx.data[key] !== value) { + if (affectedNodes.indexOf(datasource.nodeId) === -1) { + affectedNodes.push(datasource.nodeId); + } + node.data.nodeCtx.data[key] = value; + } + } + }); + } + affectedNodes.forEach((nodeId) => { + const node: HierarchyNavTreeNode = this.nodeEditCallbacks.getNode(nodeId); + if (node) { + this.updateNodeStyle(this.nodesMap[nodeId]); + } else { + this.pendingUpdateNodeTasks[nodeId] = () => { + this.updateNodeStyle(this.nodesMap[nodeId]); + }; + } + }); + } + + public loadNodes: LoadNodesCallback = (node, cb) => { + if (node.id === '#') { + const sortOrder: SortOrder = { property: 'name', direction: Direction.ASC }; + const pageLink = new PageLink(100, 0, null, sortOrder); + this.edgeService.getTenantEdgeInfos(pageLink).subscribe( + (edges) => { + cb(this.edgesToNodes(node.id, edges.data)) + }); + } else if (node.data.type === 'edge') { + const edge = node.data.entity; + cb(this.loadNodesForEdge(node.id, edge)); + } else if (node.data.type === 'edgeGroups') { + const pageLink = new PageLink(100); + this.entityService.getAssignedToEdgeEntitiesByType(node, pageLink).subscribe( + (entities) => { + if (entities.data.length > 0) { + cb(this.edgesToNodes(node.id, entities.data)); + } else { + cb([]); + } + } + ) + } + } + + private loadNodesForEdge(parentNodeId: string, edge: EdgeInfo): EdgeOverviewNode[] { + const nodes: EdgeOverviewNode[] = []; + const nodesMap = {}; + this.edgeGroupsNodesMap[parentNodeId] = nodesMap; + edgeGroupsTypes.forEach((entityType) => { + const node: EdgeOverviewNode = { + id: (++this.nodeIdCounter)+'', + icon: false, + text: edgeGroupsNodeText(this.translateService, entityType), + children: true, + data: { + type: 'edgeGroups', + entityType, + edge, + internalId: edge.id.id + '_' + entityType + } as EdgeGroupsNodeData + }; + nodes.push(node); + nodesMap[entityType] = node.id; + }); + return nodes; + } + + private createEdgeNode(parentNodeId: string, edge: Edge): EdgeOverviewNode { + let nodesMap = this.edgeNodesMap[parentNodeId]; + if (!nodesMap) { + nodesMap = {}; + this.edgeNodesMap[parentNodeId] = nodesMap; + } + const node: EdgeOverviewNode = { + id: (++this.nodeIdCounter)+'', + icon: false, + text: edgeNodeText(edge), + children: parentNodeId === '#', + state: { + disabled: false + }, + data: { + type: 'edge', + entity: edge, + internalId: edge.id.id + } as EdgeNodeData + }; + nodesMap[edge.id.id] = node.id; + return node; + } + + private edgesToNodes(parentNodeId: string, edges: Array): EdgeOverviewNode[] { + const nodes: EdgeOverviewNode[] = []; + this.edgeNodesMap[parentNodeId] = {}; + if (edges) { + edges.forEach((edge) => { + const node = this.createEdgeNode(parentNodeId, edge); + nodes.push(node); + }); + } + return nodes; + } + + public onNodeSelected: NodeSelectedCallback = (node, event) => { + let nodeId; + if (!node) { + nodeId = -1; + } else { + nodeId = node.id; + } + if (nodeId !== -1) { + const selectedNode = this.nodesMap[nodeId]; + if (selectedNode) { + const descriptors = this.ctx.actionsApi.getActionDescriptors('nodeSelected'); + if (descriptors.length) { + const entity = selectedNode.data.nodeCtx.entity; + this.ctx.actionsApi.handleWidgetAction(event, descriptors[0], entity.id, entity.name, { nodeCtx: selectedNode.data.nodeCtx }); + } + } + } + } + + public onNodesInserted: NodesInsertedCallback = (nodes) => { + if (nodes) { + nodes.forEach((nodeId) => { + const task = this.pendingUpdateNodeTasks[nodeId]; + if (task) { + task(); + delete this.pendingUpdateNodeTasks[nodeId]; + } + }); + } + } + + public searchCallback: NodeSearchCallback = (searchText, node) => { + const theNode = this.nodesMap[node.id]; + if (theNode && theNode.data.searchText) { + return theNode.data.searchText.includes(searchText.toLowerCase()); + } + return false; + } + + private updateNodeStyle(node: HierarchyNavTreeNode) { + const newText = this.prepareNodeText(node); + if (node.text !== newText) { + node.text = newText; + this.nodeEditCallbacks.updateNode(node.id, node.text); + } + const newDisabled = this.nodeDisabledFunction(node.data.nodeCtx); + if (node.state.disabled !== newDisabled) { + node.state.disabled = newDisabled; + if (node.state.disabled) { + this.nodeEditCallbacks.disableNode(node.id); + } else { + this.nodeEditCallbacks.enableNode(node.id); + } + } + const newHasChildren = this.nodeHasChildrenFunction(node.data.nodeCtx); + if (node.children !== newHasChildren) { + node.children = newHasChildren; + this.nodeEditCallbacks.setNodeHasChildren(node.id, node.children); + } + } + + private prepareNodes(nodes: HierarchyNavTreeNode[]): HierarchyNavTreeNode[] { + nodes = nodes.filter((node) => node !== null); + nodes.sort((node1, node2) => this.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx)); + return nodes; + } + + private prepareNodeText(node: HierarchyNavTreeNode): string { + const nodeIcon = this.prepareNodeIcon(node.data.nodeCtx); + const nodeText = this.nodeTextFunction(node.data.nodeCtx); + node.data.searchText = nodeText ? nodeText.replace(/<[^>]+>/g, '').toLowerCase() : ''; + return nodeIcon + nodeText; + } + + private prepareNodeIcon(nodeCtx: HierarchyNodeContext): string { + let iconInfo = this.nodeIconFunction(nodeCtx); + if (iconInfo) { + if (iconInfo === 'default') { + iconInfo = defaultNodeIconFunction(nodeCtx); + } + if (iconInfo && iconInfo !== 'default' && (iconInfo.iconUrl || iconInfo.materialIcon)) { + if (iconInfo.materialIcon) { + return materialIconHtml(iconInfo.materialIcon); + } else { + return iconUrlHtml(iconInfo.iconUrl); + } + } else { + return ''; + } + } else { + return ''; + } + } + + private datasourceToNode(datasource: HierarchyNodeDatasource, + data: DatasourceData[], + parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode { + const node: HierarchyNavTreeNode = { + id: (++this.nodeIdCounter) + '' + }; + this.nodesMap[node.id] = node; + datasource.nodeId = node.id; + node.icon = false; + const nodeCtx: HierarchyNodeContext = { + parentNodeCtx, + entity: { + id: { + id: datasource.entityId, + entityType: datasource.entityType + }, + name: datasource.entityName, + label: datasource.entityLabel ? datasource.entityLabel : datasource.entityName + }, + data: {} + }; + datasource.dataKeys.forEach((dataKey, index) => { + const keyData = data[index].data; + if (keyData && keyData.length && keyData[0].length > 1) { + nodeCtx.data[dataKey.label] = keyData[0][1]; + } else { + nodeCtx.data[dataKey.label] = ''; + } + }); + nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1; + node.data = { + datasource, + nodeCtx + }; + node.state = { + disabled: this.nodeDisabledFunction(node.data.nodeCtx), + opened: this.nodeOpenedFunction(node.data.nodeCtx) + }; + node.text = this.prepareNodeText(node); + node.children = this.nodeHasChildrenFunction(node.data.nodeCtx); + return node; + } + + private loadChildren(parentNode: HierarchyNavTreeNode, datasource: HierarchyNodeDatasource, childrenNodesLoadCb: NodesCallback) { + const nodeCtx = parentNode.data.nodeCtx; + nodeCtx.childrenNodesLoaded = false; + const entityFilter = this.prepareNodeRelationsQueryFilter(nodeCtx); + const childrenDatasource = { + dataKeys: datasource.dataKeys, + type: DatasourceType.entity, + filterId: datasource.filterId, + entityFilter + } as HierarchyNodeDatasource; + const subscriptionOptions: WidgetSubscriptionOptions = { + type: widgetType.latest, + datasources: [childrenDatasource], + callbacks: { + onSubscriptionMessage: (subscription, message) => { + this.ctx.showToast(message.severity, message.message, undefined, + 'bottom', 'left', this.toastTargetId); + }, + onInitialPageDataChanged: (subscription) => { + this.ctx.subscriptionApi.removeSubscription(subscription.id); + this.nodeEditCallbacks.refreshNode(parentNode.id); + }, + onDataUpdated: subscription => { + if (nodeCtx.childrenNodesLoaded) { + this.updateNodeData(subscription.data); + } else { + const datasourcesPageData = subscription.datasourcePages[0]; + const dataPageData = subscription.dataPages[0]; + const childNodes: HierarchyNavTreeNode[] = []; + datasourcesPageData.data.forEach((childDatasource, index) => { + childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, dataPageData.data[index])); + }); + nodeCtx.childrenNodesLoaded = true; + childrenNodesLoadCb(this.prepareNodes(childNodes)); + } + } + } + }; + this.ctx.subscriptionApi.createSubscription(subscriptionOptions, true); + } + + private prepareNodeRelationQuery(nodeCtx: HierarchyNodeContext): EntityRelationsQuery { + let relationQuery = this.nodeRelationQueryFunction(nodeCtx); + if (relationQuery && relationQuery === 'default') { + relationQuery = defaultNodeRelationQueryFunction(nodeCtx); + } + return relationQuery as EntityRelationsQuery; + } + + private prepareNodeRelationsQueryFilter(nodeCtx: HierarchyNodeContext): EntityFilter { + const relationQuery = this.prepareNodeRelationQuery(nodeCtx); + return { + rootEntity: { + id: relationQuery.parameters.rootId, + entityType: relationQuery.parameters.rootType + }, + direction: relationQuery.parameters.direction, + filters: relationQuery.filters, + maxLevel: relationQuery.parameters.maxLevel, + fetchLastLevelOnly: relationQuery.parameters.fetchLastLevelOnly, + type: AliasFilterType.relationsQuery + } as RelationsQueryFilter; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts new file mode 100644 index 0000000000..a838223098 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts @@ -0,0 +1,260 @@ +/// +/// 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 { BaseData } from '@shared/models/base-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { NavTreeNode, NodesCallback } from '@shared/components/nav-tree.component'; +import { Datasource } from '@shared/models/widget.models'; +import { isDefined, isUndefined } from '@core/utils'; +import { EntityRelationsQuery, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { Edge } from "@shared/models/edge.models"; +import { TranslateService } from "@ngx-translate/core"; + +export interface EdgesOverviewWidgetSettings { + nodeRelationQueryFunction: string; + nodeHasChildrenFunction: string; + nodeOpenedFunction: string; + nodeDisabledFunction: string; + nodeIconFunction: string; + nodeTextFunction: string; + nodesSortFunction: string; +} + +export interface HierarchyNodeContext { + parentNodeCtx?: HierarchyNodeContext; + entity: BaseData; + childrenNodesLoaded?: boolean; + level?: number; + data: {[key: string]: any}; +} + +export interface HierarchyNavTreeNode extends NavTreeNode { + data?: { + datasource: HierarchyNodeDatasource; + nodeCtx: HierarchyNodeContext; + searchText?: string; + }; +} + +export interface HierarchyNodeDatasource extends Datasource { + nodeId: string; +} + +export interface HierarchyNodeIconInfo { + iconUrl?: string; + materialIcon?: string; +} + +export type NodeRelationQueryFunction = (nodeCtx: HierarchyNodeContext) => EntityRelationsQuery | 'default'; +export type NodeTextFunction = (nodeCtx: HierarchyNodeContext) => string; +export type NodeDisabledFunction = (nodeCtx: HierarchyNodeContext) => boolean; +export type NodeIconFunction = (nodeCtx: HierarchyNodeContext) => HierarchyNodeIconInfo | 'default'; +export type NodeOpenedFunction = (nodeCtx: HierarchyNodeContext) => boolean; +export type NodeHasChildrenFunction = (nodeCtx: HierarchyNodeContext) => boolean; +export type NodesSortFunction = (nodeCtx1: HierarchyNodeContext, nodeCtx2: HierarchyNodeContext) => number; + +export function loadNodeCtxFunction any>(functionBody: string, argNames: string, ...args: any[]): F { + let nodeCtxFunction: F = null; + if (isDefined(functionBody) && functionBody.length) { + try { + nodeCtxFunction = new Function(argNames, functionBody) as F; + const res = nodeCtxFunction.apply(null, args); + if (isUndefined(res)) { + nodeCtxFunction = null; + } + } catch (e) { + nodeCtxFunction = null; + } + } + return nodeCtxFunction; +} + +export function materialIconHtml(materialIcon: string): string { + return '' + materialIcon + ''; +} + +export function iconUrlHtml(iconUrl: string): string { + return '
 
'; +} + +export function edgeGroupsNodeText(translate: TranslateService, entityType: EntityType): string { + const nodeIcon = materialIconByEntityType(entityType); + const nodeText = textForEdgeGroupsType(translate, entityType); + return nodeIcon + nodeText; +} + +export function edgeNodeText(edge: Edge): string { + const nodeIcon = materialIconByEntityType(edge.id.entityType); + const nodeText = edge.name; + return nodeIcon + nodeText; +} + +export function materialIconByEntityType(entityType: EntityType): string { + let materialIcon = 'insert_drive_file'; + switch (entityType) { + case EntityType.DEVICE: + materialIcon = 'devices_other'; + break; + case EntityType.ASSET: + materialIcon = 'domain'; + break; + case EntityType.CUSTOMER: + materialIcon = 'supervisor_account'; + break; + case EntityType.USER: + materialIcon = 'account_circle'; + break; + case EntityType.DASHBOARD: + materialIcon = 'dashboards'; + break; + case EntityType.ENTITY_VIEW: + materialIcon = 'view_quilt'; + break; + case EntityType.RULE_CHAIN: + materialIcon = 'settings_ethernet'; + break; + case EntityType.EDGE: + materialIcon = 'router'; + break; + } + return '' + materialIcon + ''; +} + +export function textForEdgeGroupsType(translate: TranslateService, entityType: EntityType): string { + let textForEdgeGroupsType: string = ''; + switch (entityType) { + case EntityType.DEVICE: + textForEdgeGroupsType = 'device.devices'; + break; + case EntityType.ASSET: + textForEdgeGroupsType = 'asset.assets'; + break; + case EntityType.DASHBOARD: + textForEdgeGroupsType = 'dashboard.dashboards'; + break; + case EntityType.ENTITY_VIEW: + textForEdgeGroupsType = 'entity-view.entity-views'; + break; + case EntityType.RULE_CHAIN: + textForEdgeGroupsType = 'rulechain.rulechains'; + break; + } + return translate.instant(textForEdgeGroupsType); +} + +export const defaultNodeRelationQueryFunction: NodeRelationQueryFunction = nodeCtx => { + const entity = nodeCtx.entity; + const query: EntityRelationsQuery = { + parameters: { + rootId: entity.id.id, + rootType: entity.id.entityType as EntityType, + direction: EntitySearchDirection.FROM, + relationTypeGroup: RelationTypeGroup.COMMON, + maxLevel: 1 + }, + filters: [ + { + relationType: 'Contains', + entityTypes: [] + } + ] + }; + return query; +}; + +export const edgeGroupsTypes: EntityType[] = [ + EntityType.ASSET, + EntityType.DEVICE, + EntityType.ENTITY_VIEW, + EntityType.DASHBOARD, + EntityType.RULE_CHAIN +] + +export const defaultNodeIconFunction: NodeIconFunction = nodeCtx => { + let materialIcon = 'insert_drive_file'; + const entity = nodeCtx.entity; + if (entity && entity.id && entity.id.entityType) { + switch (entity.id.entityType as EntityType | string) { + case 'function': + materialIcon = 'functions'; + break; + case EntityType.DEVICE: + materialIcon = 'devices_other'; + break; + case EntityType.ASSET: + materialIcon = 'domain'; + break; + case EntityType.TENANT: + materialIcon = 'supervisor_account'; + break; + case EntityType.CUSTOMER: + materialIcon = 'supervisor_account'; + break; + case EntityType.USER: + materialIcon = 'account_circle'; + break; + case EntityType.DASHBOARD: + materialIcon = 'dashboards'; + break; + case EntityType.ALARM: + materialIcon = 'notifications_active'; + break; + case EntityType.ENTITY_VIEW: + materialIcon = 'view_quilt'; + break; + } + } + return { + materialIcon + }; +}; + +export const defaultNodeOpenedFunction: NodeOpenedFunction = nodeCtx => { + return nodeCtx.level <= 4; +}; + +export const defaultNodesSortFunction: NodesSortFunction = (nodeCtx1, nodeCtx2) => { + let result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); + if (result === 0) { + result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name); + } + return result; +}; + +export interface EdgeOverviewNode extends NavTreeNode { + data?: EdgeOverviewNodeData; +} + +export type EdgeOverviewNodeData = EdgeGroupsNodeData | EdgeNodeData; + +export interface EdgeGroupsNodeData extends BaseEdgeOverviewNodeData { + type: 'edgeGroups'; + entityType: EntityType; + edge: Edge; +} + +export interface EdgeNodeData extends BaseEdgeOverviewNodeData { + type: 'edge'; + entity: Edge; +} + +export interface BaseEdgeOverviewNodeData { + type: EdgeOverviewNodeType; + internalId: string; +} + +export type EdgeOverviewNodeType = 'edge' | 'edgeGroups'; From 0ab5f30be4cf10bfb7db5e21dad7c832aa872467 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Wed, 30 Dec 2020 15:34:43 +0200 Subject: [PATCH 04/17] Edge overview widget ready --- .../system/widget_bundles/edge_widgets.json | 8 +- .../lib/edges-overview-widget.component.html | 3 + .../lib/edges-overview-widget.component.scss | 4 + .../lib/edges-overview-widget.component.ts | 155 ++++-------------- 4 files changed, 40 insertions(+), 130 deletions(-) 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 5e66e11d55..9cabfe53fb 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 @@ -1,15 +1,15 @@ { "widgetsBundle": { "alias": "edges_widgets", - "title": "Edge Instances widgets", + "title": "Edge widgets", "image": null }, "widgetTypes": [ { "alias": "edges_overview", - "name": "Edge Instances Overview", + "name": "Edges Quick Overview", "descriptor": { - "type": "static", + "type": "latest", "sizeX": 7.5, "sizeY": 5, "resources": [], @@ -18,7 +18,7 @@ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.edgesOverviewWidget.onDataUpdated();\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: \\\"FROM\\\",\\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\":\"Edge Instances 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\":{}}" + "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: \\\"FROM\\\",\\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\":\"Edge Quick Overview Widget\",\"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-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html index 355b3538c9..859e79a95c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html @@ -37,6 +37,9 @@ +
+ {{ customerTitle }} +
; + private data: Array>; private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {}; private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {}; @@ -113,7 +118,7 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni private searchAction: WidgetAction = { name: 'action.search', - show: true, + show: false, icon: 'search', onAction: () => { this.enterFilterMode(); @@ -127,7 +132,8 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni private translateService: TranslateService, private overlay: Overlay, private viewContainerRef: ViewContainerRef, - private utils: UtilsService) { + private utils: UtilsService, + private cd: ChangeDetectorRef) { super(store); } @@ -136,6 +142,10 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni this.settings = this.ctx.settings; this.widgetConfig = this.ctx.widgetConfig; this.subscription = this.ctx.defaultSubscription; + this.datasources = this.subscription.datasources as Array; + this.data = this.subscription.dataPages[0].data; + this.ctx.widgetTitle = this.datasources[0].entity.name; + this.getCustomerTitle(this.datasources[0].entity.id.id); this.initializeConfig(); this.ctx.updateWidgetParams(); } @@ -265,16 +275,10 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni public loadNodes: LoadNodesCallback = (node, cb) => { if (node.id === '#') { - const sortOrder: SortOrder = { property: 'name', direction: Direction.ASC }; - const pageLink = new PageLink(100, 0, null, sortOrder); - this.edgeService.getTenantEdgeInfos(pageLink).subscribe( - (edges) => { - cb(this.edgesToNodes(node.id, edges.data)) - }); - } else if (node.data.type === 'edge') { - const edge = node.data.entity; - cb(this.loadNodesForEdge(node.id, edge)); - } else if (node.data.type === 'edgeGroups') { + const edge: BaseData = this.datasources[0].entity; + cb(this.loadNodesForEdge(edge.id.id, edge)); + } + else if (node.data.type === 'edgeGroups') { const pageLink = new PageLink(100); this.entityService.getAssignedToEdgeEntitiesByType(node, pageLink).subscribe( (entities) => { @@ -288,7 +292,7 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni } } - private loadNodesForEdge(parentNodeId: string, edge: EdgeInfo): EdgeOverviewNode[] { + private loadNodesForEdge(parentNodeId: string, edge: any): EdgeOverviewNode[] { const nodes: EdgeOverviewNode[] = []; const nodesMap = {}; this.edgeGroupsNodesMap[parentNodeId] = nodesMap; @@ -408,12 +412,6 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni } } - private prepareNodes(nodes: HierarchyNavTreeNode[]): HierarchyNavTreeNode[] { - nodes = nodes.filter((node) => node !== null); - nodes.sort((node1, node2) => this.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx)); - return nodes; - } - private prepareNodeText(node: HierarchyNavTreeNode): string { const nodeIcon = this.prepareNodeIcon(node.data.nodeCtx); const nodeText = this.nodeTextFunction(node.data.nodeCtx); @@ -441,110 +439,15 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni } } - private datasourceToNode(datasource: HierarchyNodeDatasource, - data: DatasourceData[], - parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode { - const node: HierarchyNavTreeNode = { - id: (++this.nodeIdCounter) + '' - }; - this.nodesMap[node.id] = node; - datasource.nodeId = node.id; - node.icon = false; - const nodeCtx: HierarchyNodeContext = { - parentNodeCtx, - entity: { - id: { - id: datasource.entityId, - entityType: datasource.entityType - }, - name: datasource.entityName, - label: datasource.entityLabel ? datasource.entityLabel : datasource.entityName - }, - data: {} - }; - datasource.dataKeys.forEach((dataKey, index) => { - const keyData = data[index].data; - if (keyData && keyData.length && keyData[0].length > 1) { - nodeCtx.data[dataKey.label] = keyData[0][1]; - } else { - nodeCtx.data[dataKey.label] = ''; - } - }); - nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1; - node.data = { - datasource, - nodeCtx - }; - node.state = { - disabled: this.nodeDisabledFunction(node.data.nodeCtx), - opened: this.nodeOpenedFunction(node.data.nodeCtx) - }; - node.text = this.prepareNodeText(node); - node.children = this.nodeHasChildrenFunction(node.data.nodeCtx); - return node; - } - - private loadChildren(parentNode: HierarchyNavTreeNode, datasource: HierarchyNodeDatasource, childrenNodesLoadCb: NodesCallback) { - const nodeCtx = parentNode.data.nodeCtx; - nodeCtx.childrenNodesLoaded = false; - const entityFilter = this.prepareNodeRelationsQueryFilter(nodeCtx); - const childrenDatasource = { - dataKeys: datasource.dataKeys, - type: DatasourceType.entity, - filterId: datasource.filterId, - entityFilter - } as HierarchyNodeDatasource; - const subscriptionOptions: WidgetSubscriptionOptions = { - type: widgetType.latest, - datasources: [childrenDatasource], - callbacks: { - onSubscriptionMessage: (subscription, message) => { - this.ctx.showToast(message.severity, message.message, undefined, - 'bottom', 'left', this.toastTargetId); - }, - onInitialPageDataChanged: (subscription) => { - this.ctx.subscriptionApi.removeSubscription(subscription.id); - this.nodeEditCallbacks.refreshNode(parentNode.id); - }, - onDataUpdated: subscription => { - if (nodeCtx.childrenNodesLoaded) { - this.updateNodeData(subscription.data); - } else { - const datasourcesPageData = subscription.datasourcePages[0]; - const dataPageData = subscription.dataPages[0]; - const childNodes: HierarchyNavTreeNode[] = []; - datasourcesPageData.data.forEach((childDatasource, index) => { - childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, dataPageData.data[index])); - }); - nodeCtx.childrenNodesLoaded = true; - childrenNodesLoadCb(this.prepareNodes(childNodes)); - } + private getCustomerTitle(edgeId) { + this.edgeService.getEdgeInfo(edgeId).subscribe( + (edge) => { + if (edge.customerTitle) { + this.customerTitle = this.translateService.instant('edge.assigned-to-customer') + ': ' + edge.customerTitle; + } else { + this.customerTitle = null; } - } - }; - this.ctx.subscriptionApi.createSubscription(subscriptionOptions, true); - } - - private prepareNodeRelationQuery(nodeCtx: HierarchyNodeContext): EntityRelationsQuery { - let relationQuery = this.nodeRelationQueryFunction(nodeCtx); - if (relationQuery && relationQuery === 'default') { - relationQuery = defaultNodeRelationQueryFunction(nodeCtx); - } - return relationQuery as EntityRelationsQuery; - } - - private prepareNodeRelationsQueryFilter(nodeCtx: HierarchyNodeContext): EntityFilter { - const relationQuery = this.prepareNodeRelationQuery(nodeCtx); - return { - rootEntity: { - id: relationQuery.parameters.rootId, - entityType: relationQuery.parameters.rootType - }, - direction: relationQuery.parameters.direction, - filters: relationQuery.filters, - maxLevel: relationQuery.parameters.maxLevel, - fetchLastLevelOnly: relationQuery.parameters.fetchLastLevelOnly, - type: AliasFilterType.relationsQuery - } as RelationsQueryFilter; + this.cd.detectChanges(); + }); } } From e4e5cb35dc59bd192246f40032e821dfe06179da Mon Sep 17 00:00:00 2001 From: Artem Babak Date: Thu, 31 Dec 2020 12:01:11 +0200 Subject: [PATCH 05/17] Edges Quick Overview Widget --- .../system/widget_bundles/edge_widgets.json | 10 +- .../lib/edges-overview-widget.component.html | 24 -- .../lib/edges-overview-widget.component.ts | 323 +++--------------- .../lib/edges-overview-widget.models.ts | 163 +-------- 4 files changed, 54 insertions(+), 466 deletions(-) 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 9cabfe53fb..74e6138827 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 @@ -15,11 +15,11 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.edgesOverviewWidget.onDataUpdated();\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: \\\"FROM\\\",\\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\":\"Edge Quick Overview Widget\",\"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};\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\":{}}" } } ] -} \ No newline at end of file +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html index 859e79a95c..75baaf6340 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html @@ -17,37 +17,13 @@ -->
- -
- - -   - - - -
-
{{ customerTitle }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts index bceef70764..05aa1b7ac4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts @@ -14,63 +14,46 @@ /// limitations under the License. /// -import { ChangeDetectorRef, AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; -import { DatasourceData, DatasourceType, WidgetConfig, widgetType } from '@shared/models/widget.models'; -import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { WidgetConfig } from '@shared/models/widget.models'; +import { IWidgetSubscription } from '@core/api/widget-api.models'; import { UtilsService } from '@core/services/utils.service'; import cssjs from '@core/css/css'; -import { fromEvent, Observable } from 'rxjs'; -import { debounceTime, distinctUntilChanged, map, mergeMap, tap } from 'rxjs/operators'; +import { fromEvent } from 'rxjs'; +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { constructTableCssString } from '@home/components/widget/lib/table-widget.models'; import { Overlay } from '@angular/cdk/overlay'; import { LoadNodesCallback, - NavTreeEditCallbacks, - NodesCallback, - NodeSearchCallback, - NodeSelectedCallback, - NodesInsertedCallback + NavTreeEditCallbacks } from '@shared/components/nav-tree.component'; import { EntityType } from '@shared/models/entity-type.models'; -import { deepClone, hashCode } from '@core/utils'; +import { hashCode } from '@core/utils'; import { - defaultNodeIconFunction, - defaultNodeOpenedFunction, - defaultNodeRelationQueryFunction, - defaultNodesSortFunction, - EdgeGroupsNodeData, + EdgeGroupNodeData, edgeGroupsNodeText, - edgeGroupsTypes, - EdgeNodeData, + edgeGroupsTypes, EntityNodeData, edgeNodeText, - EdgeOverviewNode, EdgesOverviewWidgetSettings, - HierarchyNavTreeNode, - HierarchyNodeContext, - HierarchyNodeDatasource, - iconUrlHtml, - loadNodeCtxFunction, - materialIconHtml, - NodeDisabledFunction, - NodeHasChildrenFunction, - NodeIconFunction, - NodeOpenedFunction, - NodeRelationQueryFunction, - NodesSortFunction, - NodeTextFunction + EdgeOverviewNode, EntityNodeDatasource } from '@home/components/widget/lib/edges-overview-widget.models'; -import { EntityRelationsQuery } from '@shared/models/relation.models'; -import { AliasFilterType, RelationsQueryFilter } from '@shared/models/alias.models'; -import { EntityFilter } from '@shared/models/query/query.models'; import { EdgeService } from "@core/http/edge.service"; import { EntityService } from "@core/http/entity.service"; import { TranslateService } from "@ngx-translate/core"; -import { Direction, SortOrder } from "@shared/models/page/sort-order"; import { PageLink } from "@shared/models/page/page-link"; -import { Edge, EdgeInfo } from "@shared/models/edge.models"; +import { Edge } from "@shared/models/edge.models"; import { BaseData } from "@shared/models/base-data"; import { EntityId } from "@shared/models/id/entity-id"; @@ -79,7 +62,7 @@ import { EntityId } from "@shared/models/id/entity-id"; templateUrl: './edges-overview-widget.component.html', styleUrls: ['./edges-overview-widget.component.scss'] }) -export class EdgesOverviewWidgetComponent extends PageComponent implements OnInit, AfterViewInit { +export class EdgesOverviewWidgetComponent extends PageComponent implements OnInit { @Input() ctx: WidgetContext; @@ -87,44 +70,17 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni @ViewChild('searchInput') searchInputField: ElementRef; public toastTargetId = 'edges-overview-' + this.utils.guid(); - - public textSearchMode = false; - public textSearch = null; public customerTitle: string = null; - public nodeEditCallbacks: NavTreeEditCallbacks = {}; - - private settings: EdgesOverviewWidgetSettings; private widgetConfig: WidgetConfig; private subscription: IWidgetSubscription; - private datasources: Array; - private data: Array>; + private datasources: Array; - private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {}; - private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {}; private nodeIdCounter = 0; - private nodeRelationQueryFunction: NodeRelationQueryFunction; - private nodeIconFunction: NodeIconFunction; - private nodeTextFunction: NodeTextFunction; - private nodeDisabledFunction: NodeDisabledFunction; - private nodeOpenedFunction: NodeOpenedFunction; - private nodeHasChildrenFunction: NodeHasChildrenFunction; - private nodesSortFunction: NodesSortFunction; - private edgeNodesMap: {[parentNodeId: string]: {[edgeId: string]: string}} = {}; private edgeGroupsNodesMap: {[edgeNodeId: string]: {[groupType: string]: string}} = {}; - - private searchAction: WidgetAction = { - name: 'action.search', - show: false, - icon: 'search', - onAction: () => { - this.enterFilterMode(); - } - }; - constructor(protected store: Store, private elementRef: ElementRef, private edgeService: EdgeService, @@ -138,72 +94,19 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni } ngOnInit(): void { - this.ctx.$scope.edgesOverviewWidget = this; - this.settings = this.ctx.settings; this.widgetConfig = this.ctx.widgetConfig; this.subscription = this.ctx.defaultSubscription; - this.datasources = this.subscription.datasources as Array; - this.data = this.subscription.dataPages[0].data; - this.ctx.widgetTitle = this.datasources[0].entity.name; - this.getCustomerTitle(this.datasources[0].entity.id.id); + this.datasources = this.subscription.datasources as Array; + if (this.datasources.length > 0 && this.datasources[0].entity.id.entityType === EntityType.EDGE) { + let selectedEdge = this.datasources[0].entity; + this.getCustomerTitle(selectedEdge.id.id); + this.ctx.widgetTitle = selectedEdge.name; + } this.initializeConfig(); this.ctx.updateWidgetParams(); } - ngAfterViewInit(): void { - fromEvent(this.searchInputField.nativeElement, 'keyup') - .pipe( - debounceTime(150), - distinctUntilChanged(), - tap(() => { - this.updateSearchNodes(); - }) - ) - .subscribe(); - } - - public onDataUpdated() { - this.updateNodeData(this.subscription.data); - } - private initializeConfig() { - this.ctx.widgetActions = [this.searchAction]; - - const testNodeCtx: HierarchyNodeContext = { - entity: { - id: { - entityType: EntityType.DEVICE, - id: '123' - }, - name: 'TEST DEV1' - }, - data: {}, - level: 2 - }; - const parentNodeCtx = deepClone(testNodeCtx); - parentNodeCtx.level = 1; - testNodeCtx.parentNodeCtx = parentNodeCtx; - - this.nodeRelationQueryFunction = loadNodeCtxFunction(this.settings.nodeRelationQueryFunction, 'nodeCtx', testNodeCtx); - this.nodeIconFunction = loadNodeCtxFunction(this.settings.nodeIconFunction, 'nodeCtx', testNodeCtx); - this.nodeTextFunction = loadNodeCtxFunction(this.settings.nodeTextFunction, 'nodeCtx', testNodeCtx); - this.nodeDisabledFunction = loadNodeCtxFunction(this.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx); - this.nodeOpenedFunction = loadNodeCtxFunction(this.settings.nodeOpenedFunction, 'nodeCtx', testNodeCtx); - this.nodeHasChildrenFunction = loadNodeCtxFunction(this.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx); - - const testNodeCtx2 = deepClone(testNodeCtx); - testNodeCtx2.entity.name = 'TEST DEV2'; - - this.nodesSortFunction = loadNodeCtxFunction(this.settings.nodesSortFunction, 'nodeCtx1,nodeCtx2', testNodeCtx, testNodeCtx2); - - this.nodeRelationQueryFunction = this.nodeRelationQueryFunction || defaultNodeRelationQueryFunction; - this.nodeIconFunction = this.nodeIconFunction || defaultNodeIconFunction; - this.nodeTextFunction = this.nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name); - this.nodeDisabledFunction = this.nodeDisabledFunction || (() => false); - this.nodeOpenedFunction = this.nodeOpenedFunction || defaultNodeOpenedFunction; - this.nodeHasChildrenFunction = this.nodeHasChildrenFunction || (() => true); - this.nodesSortFunction = this.nodesSortFunction || defaultNodesSortFunction; - const cssString = constructTableCssString(this.widgetConfig); const cssParser = new cssjs(); cssParser.testMode = false; @@ -213,72 +116,14 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni $(this.elementRef.nativeElement).addClass(namespace); } - private enterFilterMode() { - this.textSearchMode = true; - this.textSearch = ''; - this.ctx.hideTitlePanel = true; - this.ctx.detectChanges(true); - setTimeout(() => { - this.searchInputField.nativeElement.focus(); - this.searchInputField.nativeElement.setSelectionRange(0, 0); - }, 10); - } - - exitFilterMode() { - this.textSearchMode = false; - this.textSearch = null; - this.updateSearchNodes(); - this.ctx.hideTitlePanel = false; - this.ctx.detectChanges(true); - } - - private updateSearchNodes() { - if (this.textSearch != null) { - this.nodeEditCallbacks.search(this.textSearch); - } else { - this.nodeEditCallbacks.clearSearch(); - } - } - - private updateNodeData(subscriptionData: Array) { - const affectedNodes: string[] = []; - if (subscriptionData) { - subscriptionData.forEach((datasourceData) => { - const datasource = datasourceData.datasource as HierarchyNodeDatasource; - if (datasource.nodeId) { - const node = this.nodesMap[datasource.nodeId]; - const key = datasourceData.dataKey.label; - let value; - if (datasourceData.data && datasourceData.data.length) { - value = datasourceData.data[0][1]; - } - if (node.data.nodeCtx.data[key] !== value) { - if (affectedNodes.indexOf(datasource.nodeId) === -1) { - affectedNodes.push(datasource.nodeId); - } - node.data.nodeCtx.data[key] = value; - } - } - }); - } - affectedNodes.forEach((nodeId) => { - const node: HierarchyNavTreeNode = this.nodeEditCallbacks.getNode(nodeId); - if (node) { - this.updateNodeStyle(this.nodesMap[nodeId]); - } else { - this.pendingUpdateNodeTasks[nodeId] = () => { - this.updateNodeStyle(this.nodesMap[nodeId]); - }; - } - }); - } - public loadNodes: LoadNodesCallback = (node, cb) => { - if (node.id === '#') { - const edge: BaseData = this.datasources[0].entity; - cb(this.loadNodesForEdge(edge.id.id, edge)); - } - else if (node.data.type === 'edgeGroups') { + var selectedEdge: BaseData = null; + if (this.datasources.length > 0 && this.datasources[0].entity && this.datasources[0].entity.id.entityType === EntityType.EDGE) { + selectedEdge = this.datasources[0].entity; + } + if (node.id === '#' && selectedEdge) { + cb(this.loadNodesForEdge(selectedEdge.id.id, selectedEdge)); + } else if (node.data && node.data.type === 'edgeGroup') { const pageLink = new PageLink(100); this.entityService.getAssignedToEdgeEntitiesByType(node, pageLink).subscribe( (entities) => { @@ -289,6 +134,8 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni } } ) + } else { + cb([]); } } @@ -303,11 +150,11 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni text: edgeGroupsNodeText(this.translateService, entityType), children: true, data: { - type: 'edgeGroups', + type: 'edgeGroup', entityType, edge, internalId: edge.id.id + '_' + entityType - } as EdgeGroupsNodeData + } as EdgeGroupNodeData }; nodes.push(node); nodesMap[entityType] = node.id; @@ -330,10 +177,10 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni disabled: false }, data: { - type: 'edge', + type: 'entity', entity: edge, internalId: edge.id.id - } as EdgeNodeData + } as EntityNodeData }; nodesMap[edge.id.id] = node.id; return node; @@ -351,94 +198,6 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni return nodes; } - public onNodeSelected: NodeSelectedCallback = (node, event) => { - let nodeId; - if (!node) { - nodeId = -1; - } else { - nodeId = node.id; - } - if (nodeId !== -1) { - const selectedNode = this.nodesMap[nodeId]; - if (selectedNode) { - const descriptors = this.ctx.actionsApi.getActionDescriptors('nodeSelected'); - if (descriptors.length) { - const entity = selectedNode.data.nodeCtx.entity; - this.ctx.actionsApi.handleWidgetAction(event, descriptors[0], entity.id, entity.name, { nodeCtx: selectedNode.data.nodeCtx }); - } - } - } - } - - public onNodesInserted: NodesInsertedCallback = (nodes) => { - if (nodes) { - nodes.forEach((nodeId) => { - const task = this.pendingUpdateNodeTasks[nodeId]; - if (task) { - task(); - delete this.pendingUpdateNodeTasks[nodeId]; - } - }); - } - } - - public searchCallback: NodeSearchCallback = (searchText, node) => { - const theNode = this.nodesMap[node.id]; - if (theNode && theNode.data.searchText) { - return theNode.data.searchText.includes(searchText.toLowerCase()); - } - return false; - } - - private updateNodeStyle(node: HierarchyNavTreeNode) { - const newText = this.prepareNodeText(node); - if (node.text !== newText) { - node.text = newText; - this.nodeEditCallbacks.updateNode(node.id, node.text); - } - const newDisabled = this.nodeDisabledFunction(node.data.nodeCtx); - if (node.state.disabled !== newDisabled) { - node.state.disabled = newDisabled; - if (node.state.disabled) { - this.nodeEditCallbacks.disableNode(node.id); - } else { - this.nodeEditCallbacks.enableNode(node.id); - } - } - const newHasChildren = this.nodeHasChildrenFunction(node.data.nodeCtx); - if (node.children !== newHasChildren) { - node.children = newHasChildren; - this.nodeEditCallbacks.setNodeHasChildren(node.id, node.children); - } - } - - private prepareNodeText(node: HierarchyNavTreeNode): string { - const nodeIcon = this.prepareNodeIcon(node.data.nodeCtx); - const nodeText = this.nodeTextFunction(node.data.nodeCtx); - node.data.searchText = nodeText ? nodeText.replace(/<[^>]+>/g, '').toLowerCase() : ''; - return nodeIcon + nodeText; - } - - private prepareNodeIcon(nodeCtx: HierarchyNodeContext): string { - let iconInfo = this.nodeIconFunction(nodeCtx); - if (iconInfo) { - if (iconInfo === 'default') { - iconInfo = defaultNodeIconFunction(nodeCtx); - } - if (iconInfo && iconInfo !== 'default' && (iconInfo.iconUrl || iconInfo.materialIcon)) { - if (iconInfo.materialIcon) { - return materialIconHtml(iconInfo.materialIcon); - } else { - return iconUrlHtml(iconInfo.iconUrl); - } - } else { - return ''; - } - } else { - return ''; - } - } - private getCustomerTitle(edgeId) { this.edgeService.getEdgeInfo(edgeId).subscribe( (edge) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts index a838223098..6c26754c52 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts @@ -14,83 +14,16 @@ /// limitations under the License. /// -import { BaseData } from '@shared/models/base-data'; -import { EntityId } from '@shared/models/id/entity-id'; -import { NavTreeNode, NodesCallback } from '@shared/components/nav-tree.component'; +import { NavTreeNode } from '@shared/components/nav-tree.component'; import { Datasource } from '@shared/models/widget.models'; -import { isDefined, isUndefined } from '@core/utils'; -import { EntityRelationsQuery, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models'; import { EntityType } from '@shared/models/entity-type.models'; import { Edge } from "@shared/models/edge.models"; import { TranslateService } from "@ngx-translate/core"; -export interface EdgesOverviewWidgetSettings { - nodeRelationQueryFunction: string; - nodeHasChildrenFunction: string; - nodeOpenedFunction: string; - nodeDisabledFunction: string; - nodeIconFunction: string; - nodeTextFunction: string; - nodesSortFunction: string; -} - -export interface HierarchyNodeContext { - parentNodeCtx?: HierarchyNodeContext; - entity: BaseData; - childrenNodesLoaded?: boolean; - level?: number; - data: {[key: string]: any}; -} - -export interface HierarchyNavTreeNode extends NavTreeNode { - data?: { - datasource: HierarchyNodeDatasource; - nodeCtx: HierarchyNodeContext; - searchText?: string; - }; -} - -export interface HierarchyNodeDatasource extends Datasource { +export interface EntityNodeDatasource extends Datasource { nodeId: string; } -export interface HierarchyNodeIconInfo { - iconUrl?: string; - materialIcon?: string; -} - -export type NodeRelationQueryFunction = (nodeCtx: HierarchyNodeContext) => EntityRelationsQuery | 'default'; -export type NodeTextFunction = (nodeCtx: HierarchyNodeContext) => string; -export type NodeDisabledFunction = (nodeCtx: HierarchyNodeContext) => boolean; -export type NodeIconFunction = (nodeCtx: HierarchyNodeContext) => HierarchyNodeIconInfo | 'default'; -export type NodeOpenedFunction = (nodeCtx: HierarchyNodeContext) => boolean; -export type NodeHasChildrenFunction = (nodeCtx: HierarchyNodeContext) => boolean; -export type NodesSortFunction = (nodeCtx1: HierarchyNodeContext, nodeCtx2: HierarchyNodeContext) => number; - -export function loadNodeCtxFunction any>(functionBody: string, argNames: string, ...args: any[]): F { - let nodeCtxFunction: F = null; - if (isDefined(functionBody) && functionBody.length) { - try { - nodeCtxFunction = new Function(argNames, functionBody) as F; - const res = nodeCtxFunction.apply(null, args); - if (isUndefined(res)) { - nodeCtxFunction = null; - } - } catch (e) { - nodeCtxFunction = null; - } - } - return nodeCtxFunction; -} - -export function materialIconHtml(materialIcon: string): string { - return '' + materialIcon + ''; -} - -export function iconUrlHtml(iconUrl: string): string { - return '
 
'; -} - export function edgeGroupsNodeText(translate: TranslateService, entityType: EntityType): string { const nodeIcon = materialIconByEntityType(entityType); const nodeText = textForEdgeGroupsType(translate, entityType); @@ -112,12 +45,6 @@ export function materialIconByEntityType(entityType: EntityType): string { case EntityType.ASSET: materialIcon = 'domain'; break; - case EntityType.CUSTOMER: - materialIcon = 'supervisor_account'; - break; - case EntityType.USER: - materialIcon = 'account_circle'; - break; case EntityType.DASHBOARD: materialIcon = 'dashboards'; break; @@ -127,9 +54,6 @@ export function materialIconByEntityType(entityType: EntityType): string { case EntityType.RULE_CHAIN: materialIcon = 'settings_ethernet'; break; - case EntityType.EDGE: - materialIcon = 'router'; - break; } return '' + materialIcon + ''; } @@ -156,26 +80,6 @@ export function textForEdgeGroupsType(translate: TranslateService, entityType: E return translate.instant(textForEdgeGroupsType); } -export const defaultNodeRelationQueryFunction: NodeRelationQueryFunction = nodeCtx => { - const entity = nodeCtx.entity; - const query: EntityRelationsQuery = { - parameters: { - rootId: entity.id.id, - rootType: entity.id.entityType as EntityType, - direction: EntitySearchDirection.FROM, - relationTypeGroup: RelationTypeGroup.COMMON, - maxLevel: 1 - }, - filters: [ - { - relationType: 'Contains', - entityTypes: [] - } - ] - }; - return query; -}; - export const edgeGroupsTypes: EntityType[] = [ EntityType.ASSET, EntityType.DEVICE, @@ -184,71 +88,20 @@ export const edgeGroupsTypes: EntityType[] = [ EntityType.RULE_CHAIN ] -export const defaultNodeIconFunction: NodeIconFunction = nodeCtx => { - let materialIcon = 'insert_drive_file'; - const entity = nodeCtx.entity; - if (entity && entity.id && entity.id.entityType) { - switch (entity.id.entityType as EntityType | string) { - case 'function': - materialIcon = 'functions'; - break; - case EntityType.DEVICE: - materialIcon = 'devices_other'; - break; - case EntityType.ASSET: - materialIcon = 'domain'; - break; - case EntityType.TENANT: - materialIcon = 'supervisor_account'; - break; - case EntityType.CUSTOMER: - materialIcon = 'supervisor_account'; - break; - case EntityType.USER: - materialIcon = 'account_circle'; - break; - case EntityType.DASHBOARD: - materialIcon = 'dashboards'; - break; - case EntityType.ALARM: - materialIcon = 'notifications_active'; - break; - case EntityType.ENTITY_VIEW: - materialIcon = 'view_quilt'; - break; - } - } - return { - materialIcon - }; -}; - -export const defaultNodeOpenedFunction: NodeOpenedFunction = nodeCtx => { - return nodeCtx.level <= 4; -}; - -export const defaultNodesSortFunction: NodesSortFunction = (nodeCtx1, nodeCtx2) => { - let result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); - if (result === 0) { - result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name); - } - return result; -}; - export interface EdgeOverviewNode extends NavTreeNode { data?: EdgeOverviewNodeData; } -export type EdgeOverviewNodeData = EdgeGroupsNodeData | EdgeNodeData; +export type EdgeOverviewNodeData = EdgeGroupNodeData | EntityNodeData; -export interface EdgeGroupsNodeData extends BaseEdgeOverviewNodeData { - type: 'edgeGroups'; +export interface EdgeGroupNodeData extends BaseEdgeOverviewNodeData { + type: 'edgeGroup'; entityType: EntityType; edge: Edge; } -export interface EdgeNodeData extends BaseEdgeOverviewNodeData { - type: 'edge'; +export interface EntityNodeData extends BaseEdgeOverviewNodeData { + type: 'entity'; entity: Edge; } @@ -257,4 +110,4 @@ export interface BaseEdgeOverviewNodeData { internalId: string; } -export type EdgeOverviewNodeType = 'edge' | 'edgeGroups'; +export type EdgeOverviewNodeType = 'entity' | 'edgeGroup'; From 8ddaa069b20d521c7ee7469c21c0058210ef80f4 Mon Sep 17 00:00:00 2001 From: Artem Babak Date: Thu, 31 Dec 2020 12:44:59 +0200 Subject: [PATCH 06/17] Edges Quick Overview Widget authority filter added --- .../lib/edges-overview-widget.component.ts | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts index 05aa1b7ac4..28045cef64 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts @@ -14,40 +14,28 @@ /// limitations under the License. /// -import { - AfterViewInit, - ChangeDetectorRef, - Component, - ElementRef, - Input, - OnInit, - ViewChild, - ViewContainerRef -} from '@angular/core'; +import { ChangeDetectorRef, Component, ElementRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; +import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetConfig } from '@shared/models/widget.models'; import { IWidgetSubscription } from '@core/api/widget-api.models'; import { UtilsService } from '@core/services/utils.service'; import cssjs from '@core/css/css'; -import { fromEvent } from 'rxjs'; -import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { constructTableCssString } from '@home/components/widget/lib/table-widget.models'; import { Overlay } from '@angular/cdk/overlay'; -import { - LoadNodesCallback, - NavTreeEditCallbacks -} from '@shared/components/nav-tree.component'; +import { LoadNodesCallback } from '@shared/components/nav-tree.component'; import { EntityType } from '@shared/models/entity-type.models'; import { hashCode } from '@core/utils'; import { EdgeGroupNodeData, edgeGroupsNodeText, - edgeGroupsTypes, EntityNodeData, + edgeGroupsTypes, edgeNodeText, - EdgeOverviewNode, EntityNodeDatasource + EdgeOverviewNode, + EntityNodeData, + EntityNodeDatasource } from '@home/components/widget/lib/edges-overview-widget.models'; import { EdgeService } from "@core/http/edge.service"; import { EntityService } from "@core/http/entity.service"; @@ -56,6 +44,8 @@ import { PageLink } from "@shared/models/page/page-link"; import { Edge } from "@shared/models/edge.models"; import { BaseData } from "@shared/models/base-data"; import { EntityId } from "@shared/models/id/entity-id"; +import { getCurrentAuthUser } from "@core/auth/auth.selectors"; +import { Authority } from "@shared/models/authority.enum"; @Component({ selector: 'tb-edges-overview-widget', @@ -143,7 +133,12 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni const nodes: EdgeOverviewNode[] = []; const nodesMap = {}; this.edgeGroupsNodesMap[parentNodeId] = nodesMap; - edgeGroupsTypes.forEach((entityType) => { + const authUser = getCurrentAuthUser(this.store); + var allowedGroupTypes: EntityType[] = edgeGroupsTypes; + if (authUser.authority === Authority.CUSTOMER_USER) { + allowedGroupTypes = edgeGroupsTypes.filter(type => type !== EntityType.RULE_CHAIN); + } + allowedGroupTypes.forEach((entityType) => { const node: EdgeOverviewNode = { id: (++this.nodeIdCounter)+'', icon: false, From b649185e9e348d40fb66327719b937e4ae0b8c5a Mon Sep 17 00:00:00 2001 From: Artem Babak Date: Thu, 31 Dec 2020 12:48:02 +0200 Subject: [PATCH 07/17] Edges Quick Overview Widget rename edges_widgets to edge_widgets --- .../src/main/data/json/system/widget_bundles/edge_widgets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 74e6138827..32047f20a4 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 @@ -1,6 +1,6 @@ { "widgetsBundle": { - "alias": "edges_widgets", + "alias": "edge_widgets", "title": "Edge widgets", "image": null }, From cf1e4bf5e9ab9b07ef9e7a688d29b9daca1fbdf0 Mon Sep 17 00:00:00 2001 From: Artem Babak Date: Thu, 31 Dec 2020 13:35:41 +0200 Subject: [PATCH 08/17] Edges widgets do not remove if edges_enable: false --- ui-ngx/src/app/core/http/widget.service.ts | 7 ------- .../pages/dashboard/dashboard-widget-select.component.ts | 9 +-------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 4bf35ca011..4440d8a4a9 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -30,9 +30,6 @@ import { filter, map, mergeMap, tap } from 'rxjs/operators'; import { WidgetTypeId } from '@shared/models/id/widget-type-id'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { ActivationEnd, Router } from '@angular/router'; -import { getCurrentAuthState } from "@core/auth/auth.selectors"; -import { Store } from "@ngrx/store"; -import { AppState } from "@core/core.state"; @Injectable({ providedIn: 'root' @@ -53,7 +50,6 @@ export class WidgetService { private utils: UtilsService, private resources: ResourcesService, private translate: TranslateService, - private store: Store, private router: Router ) { this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe( @@ -125,9 +121,6 @@ export class WidgetService { config?: RequestConfig): Observable> { return this.getBundleWidgetTypes(bundleAlias, isSystem, config).pipe( map((types) => { - if (!getCurrentAuthState(this.store).edgesSupportEnabled) { - types = types.filter(type => type.alias !== 'edges_overview') - } types = types.sort((a, b) => { let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]); if (result === 0) { diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts index 438bd905df..0c90a9bdda 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts @@ -22,9 +22,6 @@ import { WidgetService } from '@core/http/widget.service'; import { Widget, widgetType } from '@shared/models/widget.models'; import { toWidgetInfo } from '@home/models/widget-component.models'; import { DashboardCallbacks } from '../../models/dashboard-component.models'; -import { getCurrentAuthState } from "@core/auth/auth.selectors"; -import { Store } from "@ngrx/store"; -import { AppState } from "@core/core.state"; @Component({ selector: 'tb-dashboard-widget-select', @@ -52,8 +49,7 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { onWidgetClicked: this.onWidgetClicked.bind(this) }; - constructor(private widgetsService: WidgetService, - private store: Store) { + constructor(private widgetsService: WidgetService) { } ngOnInit(): void { @@ -116,9 +112,6 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { this.staticWidgetTypes.push(widget); break; } - if (!getCurrentAuthState(this.store).edgesSupportEnabled) { - this.staticWidgetTypes = this.staticWidgetTypes.filter(type => type.typeAlias !== 'edges_instances_overview') - } top += widget.sizeY; }); } From 9c5e70716c5016e79aa898fa0a98b293495069d6 Mon Sep 17 00:00:00 2001 From: Artem Babak Date: Thu, 31 Dec 2020 13:47:39 +0200 Subject: [PATCH 09/17] Edges widget cleanup --- .../lib/edges-overview-widget.component.html | 1 - .../lib/edges-overview-widget.component.ts | 22 +------------------ 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html index 75baaf6340..005afe62e6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html @@ -23,7 +23,6 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts index 28045cef64..2559b58f3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, ElementRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -22,12 +22,8 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetConfig } from '@shared/models/widget.models'; import { IWidgetSubscription } from '@core/api/widget-api.models'; import { UtilsService } from '@core/services/utils.service'; -import cssjs from '@core/css/css'; -import { constructTableCssString } from '@home/components/widget/lib/table-widget.models'; -import { Overlay } from '@angular/cdk/overlay'; import { LoadNodesCallback } from '@shared/components/nav-tree.component'; import { EntityType } from '@shared/models/entity-type.models'; -import { hashCode } from '@core/utils'; import { EdgeGroupNodeData, edgeGroupsNodeText, @@ -57,8 +53,6 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni @Input() ctx: WidgetContext; - @ViewChild('searchInput') searchInputField: ElementRef; - public toastTargetId = 'edges-overview-' + this.utils.guid(); public customerTitle: string = null; @@ -72,12 +66,9 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni private edgeGroupsNodesMap: {[edgeNodeId: string]: {[groupType: string]: string}} = {}; constructor(protected store: Store, - private elementRef: ElementRef, private edgeService: EdgeService, private entityService: EntityService, private translateService: TranslateService, - private overlay: Overlay, - private viewContainerRef: ViewContainerRef, private utils: UtilsService, private cd: ChangeDetectorRef) { super(store); @@ -92,20 +83,9 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni this.getCustomerTitle(selectedEdge.id.id); this.ctx.widgetTitle = selectedEdge.name; } - this.initializeConfig(); this.ctx.updateWidgetParams(); } - private initializeConfig() { - const cssString = constructTableCssString(this.widgetConfig); - const cssParser = new cssjs(); - cssParser.testMode = false; - const namespace = 'edges-overview-' + hashCode(cssString); - cssParser.cssPreviewNamespace = namespace; - cssParser.createStyleElement(namespace, cssString); - $(this.elementRef.nativeElement).addClass(namespace); - } - public loadNodes: LoadNodesCallback = (node, cb) => { var selectedEdge: BaseData = null; if (this.datasources.length > 0 && this.datasources[0].entity && this.datasources[0].entity.id.entityType === EntityType.EDGE) { From 61ef70c5afd1b78c5b84ffcd030bc00a4a7d3613 Mon Sep 17 00:00:00 2001 From: Artem Babak Date: Thu, 31 Dec 2020 21:09:12 +0200 Subject: [PATCH 10/17] Edge Widger customerInfo sub-header style --- .../widget/lib/edges-overview-widget.component.html | 4 ++-- .../widget/lib/edges-overview-widget.component.scss | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html index 005afe62e6..611f1fa251 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html @@ -17,8 +17,8 @@ -->
-
- {{ customerTitle }} +
+ {{ customerTitle }}
Date: Mon, 4 Jan 2021 07:22:32 +0200 Subject: [PATCH 11/17] Edge widget customerTitle refactored --- .../components/widget/lib/edges-overview-widget.component.ts | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts index 2559b58f3e..a287c52742 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts @@ -177,7 +177,7 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni this.edgeService.getEdgeInfo(edgeId).subscribe( (edge) => { if (edge.customerTitle) { - this.customerTitle = this.translateService.instant('edge.assigned-to-customer') + ': ' + edge.customerTitle; + this.customerTitle = this.translateService.instant('edge.assigned-to-customer', {customerTitle: edge.customerTitle}); } else { this.customerTitle = null; } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index bf7b38735f..26cdbe43bd 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1233,7 +1233,7 @@ "assign-to-customer-text": "Please select the customer to assign the edge(s)", "assign-edge-to-customer": "Assign Edge(s) To Customer", "assign-edge-to-customer-text": "Please select the edges to assign to the customer", - "assigned-to-customer": "Assigned to customer", + "assigned-to-customer": "Assigned to: {{customerTitle}}", "unassign-from-customer": "Unassign from customer", "assign-edges-text": "Assign { count, plural, 1 {1 edge} other {# edges} } to customer", "unassign-edge-title": "Are you sure you want to unassign the edge '{{edgeName}}'?", From 206c570abbfa9eacf5d5ec2c0a64827533376f2b Mon Sep 17 00:00:00 2001 From: deaflynx Date: Mon, 4 Jan 2021 08:32:38 +0200 Subject: [PATCH 12/17] Hide edge widget if edgesSupportEnabled() --- .../widgets-bundles-table-config.resolver.ts | 16 ++++++++++++++-- .../widgets-bundle-select.component.ts | 10 +++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts index c59dd6cc63..13a5ff9da1 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts @@ -33,11 +33,12 @@ import { WidgetsBundleComponent } from '@modules/home/pages/widget/widgets-bundl import { NULL_UUID } from '@shared/models/id/has-uuid'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { getCurrentAuthUser } from '@app/core/auth/auth.selectors'; +import { getCurrentAuthState, getCurrentAuthUser } from '@app/core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; import { DialogService } from '@core/services/dialog.service'; import { ImportExportService } from '@home/components/import-export/import-export.service'; import { Direction } from "@shared/models/page/sort-order"; +import { map } from "rxjs/operators"; @Injectable() export class WidgetsBundlesTableConfigResolver implements Resolve> { @@ -106,7 +107,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve this.translate.instant('widgets-bundle.delete-widgets-bundles-title', {count}); this.config.deleteEntitiesContent = () => this.translate.instant('widgets-bundle.delete-widgets-bundles-text'); - this.config.entitiesFetchFunction = pageLink => this.widgetsService.getWidgetBundles(pageLink); + this.config.loadEntity = id => this.widgetsService.getWidgetsBundle(id.id); this.config.saveEntity = widgetsBundle => this.widgetsService.saveWidgetsBundle(widgetsBundle); this.config.deleteEntity = id => this.widgetsService.deleteWidgetsBundle(id.id); @@ -119,6 +120,17 @@ export class WidgetsBundlesTableConfigResolver implements Resolve this.isWidgetsBundleEditable(widgetsBundle, authUser.authority); this.config.entitySelectionEnabled = (widgetsBundle) => this.isWidgetsBundleEditable(widgetsBundle, authUser.authority); this.config.detailsReadonly = (widgetsBundle) => !this.isWidgetsBundleEditable(widgetsBundle, authUser.authority); + const authState = getCurrentAuthState(this.store); + if (!authState.edgesSupportEnabled) { + this.config.entitiesFetchFunction = pageLink => this.widgetsService.getWidgetBundles(pageLink).pipe( + map((widgetBundles) => { + widgetBundles.data = widgetBundles.data.filter(widgetBundle => widgetBundle.alias !== 'edge_widgets'); + return widgetBundles; + }) + ); + } else { + this.config.entitiesFetchFunction = pageLink => this.widgetsService.getWidgetBundles(pageLink); + } return this.config; } diff --git a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts index 90952c1ddb..97542d8012 100644 --- a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts +++ b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts @@ -17,7 +17,7 @@ import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Observable } from 'rxjs'; -import { share, tap } from 'rxjs/operators'; +import { map, share, tap } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; @@ -25,6 +25,7 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetService } from '@core/http/widget.service'; import { isDefined } from '@core/utils'; import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { getCurrentAuthState } from "@core/auth/auth.selectors"; @Component({ selector: 'tb-widgets-bundle-select', @@ -94,6 +95,13 @@ export class WidgetsBundleSelectComponent implements ControlValueAccessor, OnIni } } }), + map((widgetsBundles) => { + const authState = getCurrentAuthState(this.store); + if (!authState.edgesSupportEnabled) { + widgetsBundles = widgetsBundles.filter(widgetsBundle => widgetsBundle.alias !== 'edge_widgets'); + } + return widgetsBundles; + }), share() ); } From 6d83477fe638f25f046510936a3a2ee5106f78f5 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Mon, 4 Jan 2021 12:43:46 +0200 Subject: [PATCH 13/17] Edge widget implementation with initial simulated data --- ui-ngx/src/app/core/http/entity.service.ts | 12 ++-- .../lib/edges-overview-widget.component.ts | 71 ++++++++++--------- .../lib/edges-overview-widget.models.ts | 17 ++--- 3 files changed, 49 insertions(+), 51 deletions(-) diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 95fad50c85..1e2e398991 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -1160,23 +1160,23 @@ export class EntityService { } public getAssignedToEdgeEntitiesByType(node: NavTreeNode, pageLink: PageLink): Observable> { - let edgeId = node.data.edge.id.id; + let edgeId = node.data.entity.id.id; let entitiesObservable: Observable>; switch (node.data.entityType) { case (EntityType.ASSET): - entitiesObservable = this.assetService.getEdgeAssets(edgeId, pageLink, null); + entitiesObservable = this.assetService.getEdgeAssets(edgeId, pageLink); break; case (EntityType.DEVICE): - entitiesObservable = this.deviceService.getEdgeDevices(edgeId, pageLink, null); + entitiesObservable = this.deviceService.getEdgeDevices(edgeId, pageLink); break; case (EntityType.ENTITY_VIEW): - entitiesObservable = this.entityViewService.getEdgeEntityViews(edgeId, pageLink, null); + entitiesObservable = this.entityViewService.getEdgeEntityViews(edgeId, pageLink); break; case (EntityType.DASHBOARD): - entitiesObservable = this.dashboardService.getEdgeDashboards(edgeId, pageLink, null); + entitiesObservable = this.dashboardService.getEdgeDashboards(edgeId, pageLink); break; case (EntityType.RULE_CHAIN): - entitiesObservable = this.ruleChainService.getEdgeRuleChains(edgeId, pageLink, null); + entitiesObservable = this.ruleChainService.getEdgeRuleChains(edgeId, pageLink); break; } return entitiesObservable; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts index a287c52742..60da62bab1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts @@ -28,7 +28,7 @@ import { EdgeGroupNodeData, edgeGroupsNodeText, edgeGroupsTypes, - edgeNodeText, + entityNodeText, EdgeOverviewNode, EntityNodeData, EntityNodeDatasource @@ -37,11 +37,11 @@ import { EdgeService } from "@core/http/edge.service"; import { EntityService } from "@core/http/entity.service"; import { TranslateService } from "@ngx-translate/core"; import { PageLink } from "@shared/models/page/page-link"; -import { Edge } from "@shared/models/edge.models"; -import { BaseData } from "@shared/models/base-data"; +import { BaseData, HasId } from "@shared/models/base-data"; import { EntityId } from "@shared/models/id/entity-id"; import { getCurrentAuthUser } from "@core/auth/auth.selectors"; import { Authority } from "@shared/models/authority.enum"; +import { Direction } from "@shared/models/page/sort-order"; @Component({ selector: 'tb-edges-overview-widget', @@ -62,8 +62,8 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni private nodeIdCounter = 0; - private edgeNodesMap: {[parentNodeId: string]: {[edgeId: string]: string}} = {}; - private edgeGroupsNodesMap: {[edgeNodeId: string]: {[groupType: string]: string}} = {}; + private entityNodesMap: {[parentNodeId: string]: {[edgeId: string]: string}} = {}; + private entityGroupsNodesMap: {[edgeNodeId: string]: {[groupType: string]: string}} = {}; constructor(protected store: Store, private edgeService: EdgeService, @@ -87,18 +87,23 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni } public loadNodes: LoadNodesCallback = (node, cb) => { - var selectedEdge: BaseData = null; - if (this.datasources.length > 0 && this.datasources[0].entity && this.datasources[0].entity.id.entityType === EntityType.EDGE) { - selectedEdge = this.datasources[0].entity; - } - if (node.id === '#' && selectedEdge) { - cb(this.loadNodesForEdge(selectedEdge.id.id, selectedEdge)); - } else if (node.data && node.data.type === 'edgeGroup') { - const pageLink = new PageLink(100); + if (node.id === '#' && this.datasources.length > 0) { + var selectedEdge: BaseData = null; + if (this.datasources[0].entity.id.entityType === EntityType.EDGE) { + selectedEdge = this.datasources[0].entity; + } + if (selectedEdge) { + cb(this.loadNodesForEdge(selectedEdge.id.id, selectedEdge)); + } else { + cb(this.loadNodesForEdge(this.datasources[0].entityId, this.datasources[0].entity)); + } + } else if (node.data && node.data.entity.id.entityType === EntityType.EDGE) { + const sortOrder = { property: 'createdTime', direction: Direction.DESC }; + const pageLink = new PageLink(10, 0, null, sortOrder); this.entityService.getAssignedToEdgeEntitiesByType(node, pageLink).subscribe( (entities) => { if (entities.data.length > 0) { - cb(this.edgesToNodes(node.id, entities.data)); + cb(this.entitiesToNodes(node.id, entities.data)); } else { cb([]); } @@ -109,10 +114,10 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni } } - private loadNodesForEdge(parentNodeId: string, edge: any): EdgeOverviewNode[] { + private loadNodesForEdge(parentNodeId: string, entity: BaseData): EdgeOverviewNode[] { const nodes: EdgeOverviewNode[] = []; const nodesMap = {}; - this.edgeGroupsNodesMap[parentNodeId] = nodesMap; + this.entityGroupsNodesMap[parentNodeId] = nodesMap; const authUser = getCurrentAuthUser(this.store); var allowedGroupTypes: EntityType[] = edgeGroupsTypes; if (authUser.authority === Authority.CUSTOMER_USER) { @@ -125,10 +130,9 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni text: edgeGroupsNodeText(this.translateService, entityType), children: true, data: { - type: 'edgeGroup', entityType, - edge, - internalId: edge.id.id + '_' + entityType + entity, + internalId: entity.id.id + '_' + entityType } as EdgeGroupNodeData }; nodes.push(node); @@ -137,43 +141,42 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni return nodes; } - private createEdgeNode(parentNodeId: string, edge: Edge): EdgeOverviewNode { - let nodesMap = this.edgeNodesMap[parentNodeId]; + private createEntityNode(parentNodeId: string, entity: BaseData): EdgeOverviewNode { + let nodesMap = this.entityNodesMap[parentNodeId]; if (!nodesMap) { nodesMap = {}; - this.edgeNodesMap[parentNodeId] = nodesMap; + this.entityNodesMap[parentNodeId] = nodesMap; } const node: EdgeOverviewNode = { id: (++this.nodeIdCounter)+'', icon: false, - text: edgeNodeText(edge), - children: parentNodeId === '#', + text: entityNodeText(entity), + children: false, state: { disabled: false }, data: { - type: 'entity', - entity: edge, - internalId: edge.id.id + entity: entity, + internalId: entity.id.id } as EntityNodeData }; - nodesMap[edge.id.id] = node.id; + nodesMap[entity.id.id] = node.id; return node; } - private edgesToNodes(parentNodeId: string, edges: Array): EdgeOverviewNode[] { + private entitiesToNodes(parentNodeId: string, entities: BaseData[]): EdgeOverviewNode[] { const nodes: EdgeOverviewNode[] = []; - this.edgeNodesMap[parentNodeId] = {}; - if (edges) { - edges.forEach((edge) => { - const node = this.createEdgeNode(parentNodeId, edge); + this.entityNodesMap[parentNodeId] = {}; + if (entities) { + entities.forEach((entity) => { + const node = this.createEntityNode(parentNodeId, entity); nodes.push(node); }); } return nodes; } - private getCustomerTitle(edgeId) { + private getCustomerTitle(edgeId: string) { this.edgeService.getEdgeInfo(edgeId).subscribe( (edge) => { if (edge.customerTitle) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts index 6c26754c52..ca561b8693 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts @@ -17,8 +17,8 @@ import { NavTreeNode } from '@shared/components/nav-tree.component'; import { Datasource } from '@shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; -import { Edge } from "@shared/models/edge.models"; import { TranslateService } from "@ngx-translate/core"; +import { BaseData, HasId } from "@shared/models/base-data"; export interface EntityNodeDatasource extends Datasource { nodeId: string; @@ -30,9 +30,9 @@ export function edgeGroupsNodeText(translate: TranslateService, entityType: Enti return nodeIcon + nodeText; } -export function edgeNodeText(edge: Edge): string { - const nodeIcon = materialIconByEntityType(edge.id.entityType); - const nodeText = edge.name; +export function entityNodeText(entity: any): string { + const nodeIcon = materialIconByEntityType(entity.id.entityType); + const nodeText = entity.name; return nodeIcon + nodeText; } @@ -95,19 +95,14 @@ export interface EdgeOverviewNode extends NavTreeNode { export type EdgeOverviewNodeData = EdgeGroupNodeData | EntityNodeData; export interface EdgeGroupNodeData extends BaseEdgeOverviewNodeData { - type: 'edgeGroup'; entityType: EntityType; - edge: Edge; + entity: BaseData; } export interface EntityNodeData extends BaseEdgeOverviewNodeData { - type: 'entity'; - entity: Edge; + entity: BaseData; } export interface BaseEdgeOverviewNodeData { - type: EdgeOverviewNodeType; internalId: string; } - -export type EdgeOverviewNodeType = 'entity' | 'edgeGroup'; From c519936ae9b1201c1fb7dde81467712969f99a7a Mon Sep 17 00:00:00 2001 From: deaflynx Date: Mon, 4 Jan 2021 14:28:42 +0200 Subject: [PATCH 14/17] Edge widget refactored --- ui-ngx/src/app/core/http/entity.service.ts | 5 ++- .../lib/edges-overview-widget.component.ts | 35 +++++++++---------- .../assets/locale/locale.constant-en_US.json | 3 +- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 1e2e398991..ab1fd943c9 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -1159,10 +1159,9 @@ export class EntityService { }); } - public getAssignedToEdgeEntitiesByType(node: NavTreeNode, pageLink: PageLink): Observable> { - let edgeId = node.data.entity.id.id; + public getAssignedToEdgeEntitiesByType(edgeId: string, entityType: EntityType, pageLink: PageLink): Observable> { let entitiesObservable: Observable>; - switch (node.data.entityType) { + switch (entityType) { case (EntityType.ASSET): entitiesObservable = this.assetService.getEdgeAssets(edgeId, pageLink); break; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts index 60da62bab1..28457bab8a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts @@ -19,7 +19,7 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetContext } from '@home/models/widget-component.models'; -import { WidgetConfig } from '@shared/models/widget.models'; +import { Datasource, DatasourceType, WidgetConfig } from '@shared/models/widget.models'; import { IWidgetSubscription } from '@core/api/widget-api.models'; import { UtilsService } from '@core/services/utils.service'; import { LoadNodesCallback } from '@shared/components/nav-tree.component'; @@ -41,7 +41,6 @@ import { BaseData, HasId } from "@shared/models/base-data"; import { EntityId } from "@shared/models/id/entity-id"; import { getCurrentAuthUser } from "@core/auth/auth.selectors"; import { Authority } from "@shared/models/authority.enum"; -import { Direction } from "@shared/models/page/sort-order"; @Component({ selector: 'tb-edges-overview-widget', @@ -78,29 +77,29 @@ export class EdgesOverviewWidgetComponent extends PageComponent implements OnIni this.widgetConfig = this.ctx.widgetConfig; this.subscription = this.ctx.defaultSubscription; this.datasources = this.subscription.datasources as Array; - if (this.datasources.length > 0 && this.datasources[0].entity.id.entityType === EntityType.EDGE) { - let selectedEdge = this.datasources[0].entity; - this.getCustomerTitle(selectedEdge.id.id); - this.ctx.widgetTitle = selectedEdge.name; - } this.ctx.updateWidgetParams(); } public loadNodes: LoadNodesCallback = (node, cb) => { - if (node.id === '#' && this.datasources.length > 0) { - var selectedEdge: BaseData = null; - if (this.datasources[0].entity.id.entityType === EntityType.EDGE) { - selectedEdge = this.datasources[0].entity; - } - if (selectedEdge) { + const datasource: Datasource = this.datasources[0]; + if (node.id === '#' && datasource) { + if (datasource.type === DatasourceType.entity && datasource.entity.id.entityType === EntityType.EDGE) { + var selectedEdge: BaseData = datasource.entity; + this.getCustomerTitle(selectedEdge.id.id); + this.ctx.widgetTitle = selectedEdge.name; cb(this.loadNodesForEdge(selectedEdge.id.id, selectedEdge)); + } else if (datasource.type === DatasourceType.function) { + cb(this.loadNodesForEdge(datasource.entityId, datasource.entity)); } else { - cb(this.loadNodesForEdge(this.datasources[0].entityId, this.datasources[0].entity)); + this.ctx.showErrorToast(this.translateService.instant('edge.widget-datasource-error')); + cb([]); } - } else if (node.data && node.data.entity.id.entityType === EntityType.EDGE) { - const sortOrder = { property: 'createdTime', direction: Direction.DESC }; - const pageLink = new PageLink(10, 0, null, sortOrder); - this.entityService.getAssignedToEdgeEntitiesByType(node, pageLink).subscribe( + } + else if (node.data && node.data.entity.id.entityType === EntityType.EDGE) { + const edgeId = node.data.entity.id.id; + const entityType = node.data.entityType; + const pageLink = new PageLink(datasource.pageLink.pageSize); + this.entityService.getAssignedToEdgeEntitiesByType(edgeId, entityType, pageLink).subscribe( (entities) => { if (entities.data.length > 0) { cb(this.entitiesToNodes(node.id, entities.data)); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 26cdbe43bd..1465c9e880 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1287,7 +1287,8 @@ "no-downlinks-prompt": "No downlinks found", "sync-process-started-successfully": "Sync process started successfully!", "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}}" + "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}}", + "widget-datasource-error": "This widget supports only EDGE entity" }, "edge-event": { "type-dashboard": "Dashboard", From fa5d716d3dea5cf11857b665cb0f617e7acc3979 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Mon, 4 Jan 2021 14:30:17 +0200 Subject: [PATCH 15/17] Edge widget deleted line --- ui-ngx/src/app/core/http/entity.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index ab1fd943c9..1e152a4249 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -71,7 +71,6 @@ import { alarmFields } from '@shared/models/alarm.models'; import { EdgeService } from "@core/http/edge.service"; import { ruleChainType } from "@shared/models/rule-chain.models"; import { Router } from "@angular/router"; -import { NavTreeNode } from "@shared/components/nav-tree.component"; @Injectable({ providedIn: 'root' From 746d17185c5d53c4dbae3388a39009c05058ee0e Mon Sep 17 00:00:00 2001 From: deaflynx Date: Mon, 4 Jan 2021 14:57:32 +0200 Subject: [PATCH 16/17] Edge widget message warning --- .../components/widget/lib/edges-overview-widget.component.html | 3 +++ .../components/widget/lib/edges-overview-widget.component.ts | 3 ++- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html index 611f1fa251..9a2ab9fd68 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html @@ -20,6 +20,9 @@
{{ customerTitle }}
+
+ edge.widget-datasource-error +

List of missing rule chain(s):
{{missingRuleChains}}", - "widget-datasource-error": "This widget supports only EDGE entity" + "widget-datasource-error": "This widget supports only EDGE entity datasource" }, "edge-event": { "type-dashboard": "Dashboard", From 17ecaa96604b5d41efa4181e176df4f826d9fa60 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Mon, 4 Jan 2021 15:02:53 +0200 Subject: [PATCH 17/17] Edge widget incorrect datasource type message --- .../widget/lib/edges-overview-widget.component.html | 8 ++------ .../widget/lib/edges-overview-widget.component.ts | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html index 9a2ab9fd68..47cdf0e37f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html @@ -17,12 +17,8 @@ -->
-
- {{ customerTitle }} -
-
- edge.widget-datasource-error -
+ {{ customerTitle }} + edge.widget-datasource-error