From cb6e4d13da7ece991596eaa89d2d57c572cde2d5 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Tue, 29 Dec 2020 10:41:49 +0200 Subject: [PATCH] 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; }); }