committed by
GitHub
11 changed files with 531 additions and 5 deletions
@ -0,0 +1,25 @@ |
|||||
|
{ |
||||
|
"widgetsBundle": { |
||||
|
"alias": "edge_widgets", |
||||
|
"title": "Edge widgets", |
||||
|
"image": null |
||||
|
}, |
||||
|
"widgetTypes": [ |
||||
|
{ |
||||
|
"alias": "edges_overview", |
||||
|
"name": "Edges Quick Overview", |
||||
|
"descriptor": { |
||||
|
"type": "latest", |
||||
|
"sizeX": 7.5, |
||||
|
"sizeY": 5, |
||||
|
"resources": [], |
||||
|
"templateHtml": "<tb-edges-overview-widget \n [ctx]=\"ctx\">\n</tb-edges-overview-widget>", |
||||
|
"templateCss": "", |
||||
|
"controllerScript": "self.onInit = function() {\n};\n\nself.onDestroy = function() {\n};\n", |
||||
|
"settingsSchema": "{}", |
||||
|
"dataKeySettingsSchema": "{}\n", |
||||
|
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{},\"title\":\"Edges Quick Overview\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"widgetStyle\":{},\"actions\":{}}" |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-edges-overview tb-absolute-fill" tb-toast toastTarget="{{ toastTargetId }}"> |
||||
|
<div fxFlex fxLayout="column" class="tb-absolute-fill"> |
||||
|
<span class="mat-subheader" *ngIf="customerTitle" fxLayout="row">{{ customerTitle }}</span> |
||||
|
<span class="mat-subheader" *ngIf="!edgeIsDatasource" fxLayout="row" translate>edge.widget-datasource-error</span> |
||||
|
<div fxFlex class="tb-entities-nav-tree-panel"> |
||||
|
<tb-nav-tree |
||||
|
[loadNodes]="loadNodes" |
||||
|
></tb-nav-tree> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,128 @@ |
|||||
|
/** |
||||
|
* 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-edges-overview { |
||||
|
mat-toolbar { |
||||
|
height: 60px; |
||||
|
max-height: 60px; |
||||
|
.mat-toolbar-tools { |
||||
|
height: 60px; |
||||
|
max-height: 60px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host { |
||||
|
.tb-edges-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; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.mat-subheader { |
||||
|
padding: 15px; |
||||
|
color: rgba(0,0,0,0.54); |
||||
|
font-size: 14px; |
||||
|
font-weight: 400; |
||||
|
} |
||||
|
|
||||
|
.tb-entities-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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,190 @@ |
|||||
|
///
|
||||
|
/// 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 { 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'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.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'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
import { |
||||
|
EdgeGroupNodeData, |
||||
|
edgeGroupsNodeText, |
||||
|
edgeGroupsTypes, |
||||
|
entityNodeText, |
||||
|
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"; |
||||
|
import { TranslateService } from "@ngx-translate/core"; |
||||
|
import { PageLink } from "@shared/models/page/page-link"; |
||||
|
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"; |
||||
|
|
||||
|
@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 { |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
public toastTargetId = 'edges-overview-' + this.utils.guid(); |
||||
|
public customerTitle: string = null; |
||||
|
public edgeIsDatasource: boolean = true; |
||||
|
|
||||
|
private widgetConfig: WidgetConfig; |
||||
|
private subscription: IWidgetSubscription; |
||||
|
private datasources: Array<EntityNodeDatasource>; |
||||
|
|
||||
|
private nodeIdCounter = 0; |
||||
|
|
||||
|
private entityNodesMap: {[parentNodeId: string]: {[edgeId: string]: string}} = {}; |
||||
|
private entityGroupsNodesMap: {[edgeNodeId: string]: {[groupType: string]: string}} = {}; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private edgeService: EdgeService, |
||||
|
private entityService: EntityService, |
||||
|
private translateService: TranslateService, |
||||
|
private utils: UtilsService, |
||||
|
private cd: ChangeDetectorRef) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.widgetConfig = this.ctx.widgetConfig; |
||||
|
this.subscription = this.ctx.defaultSubscription; |
||||
|
this.datasources = this.subscription.datasources as Array<EntityNodeDatasource>; |
||||
|
this.ctx.updateWidgetParams(); |
||||
|
} |
||||
|
|
||||
|
public loadNodes: LoadNodesCallback = (node, cb) => { |
||||
|
const datasource: Datasource = this.datasources[0]; |
||||
|
if (node.id === '#' && datasource) { |
||||
|
if (datasource.type === DatasourceType.entity && datasource.entity.id.entityType === EntityType.EDGE) { |
||||
|
var selectedEdge: BaseData<EntityId> = 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 { |
||||
|
this.edgeIsDatasource = false; |
||||
|
cb([]); |
||||
|
} |
||||
|
} |
||||
|
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)); |
||||
|
} else { |
||||
|
cb([]); |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
} else { |
||||
|
cb([]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private loadNodesForEdge(parentNodeId: string, entity: BaseData<HasId>): EdgeOverviewNode[] { |
||||
|
const nodes: EdgeOverviewNode[] = []; |
||||
|
const nodesMap = {}; |
||||
|
this.entityGroupsNodesMap[parentNodeId] = nodesMap; |
||||
|
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, |
||||
|
text: edgeGroupsNodeText(this.translateService, entityType), |
||||
|
children: true, |
||||
|
data: { |
||||
|
entityType, |
||||
|
entity, |
||||
|
internalId: entity.id.id + '_' + entityType |
||||
|
} as EdgeGroupNodeData |
||||
|
}; |
||||
|
nodes.push(node); |
||||
|
nodesMap[entityType] = node.id; |
||||
|
}); |
||||
|
return nodes; |
||||
|
} |
||||
|
|
||||
|
private createEntityNode(parentNodeId: string, entity: BaseData<HasId>): EdgeOverviewNode { |
||||
|
let nodesMap = this.entityNodesMap[parentNodeId]; |
||||
|
if (!nodesMap) { |
||||
|
nodesMap = {}; |
||||
|
this.entityNodesMap[parentNodeId] = nodesMap; |
||||
|
} |
||||
|
const node: EdgeOverviewNode = { |
||||
|
id: (++this.nodeIdCounter)+'', |
||||
|
icon: false, |
||||
|
text: entityNodeText(entity), |
||||
|
children: false, |
||||
|
state: { |
||||
|
disabled: false |
||||
|
}, |
||||
|
data: { |
||||
|
entity: entity, |
||||
|
internalId: entity.id.id |
||||
|
} as EntityNodeData |
||||
|
}; |
||||
|
nodesMap[entity.id.id] = node.id; |
||||
|
return node; |
||||
|
} |
||||
|
|
||||
|
private entitiesToNodes(parentNodeId: string, entities: BaseData<HasId>[]): EdgeOverviewNode[] { |
||||
|
const nodes: EdgeOverviewNode[] = []; |
||||
|
this.entityNodesMap[parentNodeId] = {}; |
||||
|
if (entities) { |
||||
|
entities.forEach((entity) => { |
||||
|
const node = this.createEntityNode(parentNodeId, entity); |
||||
|
nodes.push(node); |
||||
|
}); |
||||
|
} |
||||
|
return nodes; |
||||
|
} |
||||
|
|
||||
|
private getCustomerTitle(edgeId: string) { |
||||
|
this.edgeService.getEdgeInfo(edgeId).subscribe( |
||||
|
(edge) => { |
||||
|
if (edge.customerTitle) { |
||||
|
this.customerTitle = this.translateService.instant('edge.assigned-to-customer', {customerTitle: edge.customerTitle}); |
||||
|
} else { |
||||
|
this.customerTitle = null; |
||||
|
} |
||||
|
this.cd.detectChanges(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,108 @@ |
|||||
|
///
|
||||
|
/// 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 { NavTreeNode } from '@shared/components/nav-tree.component'; |
||||
|
import { Datasource } from '@shared/models/widget.models'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
import { TranslateService } from "@ngx-translate/core"; |
||||
|
import { BaseData, HasId } from "@shared/models/base-data"; |
||||
|
|
||||
|
export interface EntityNodeDatasource extends Datasource { |
||||
|
nodeId: string; |
||||
|
} |
||||
|
|
||||
|
export function edgeGroupsNodeText(translate: TranslateService, entityType: EntityType): string { |
||||
|
const nodeIcon = materialIconByEntityType(entityType); |
||||
|
const nodeText = textForEdgeGroupsType(translate, entityType); |
||||
|
return nodeIcon + nodeText; |
||||
|
} |
||||
|
|
||||
|
export function entityNodeText(entity: any): string { |
||||
|
const nodeIcon = materialIconByEntityType(entity.id.entityType); |
||||
|
const nodeText = entity.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.DASHBOARD: |
||||
|
materialIcon = 'dashboards'; |
||||
|
break; |
||||
|
case EntityType.ENTITY_VIEW: |
||||
|
materialIcon = 'view_quilt'; |
||||
|
break; |
||||
|
case EntityType.RULE_CHAIN: |
||||
|
materialIcon = 'settings_ethernet'; |
||||
|
break; |
||||
|
} |
||||
|
return '<mat-icon class="node-icon material-icons" role="img" aria-hidden="false">' + materialIcon + '</mat-icon>'; |
||||
|
} |
||||
|
|
||||
|
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 edgeGroupsTypes: EntityType[] = [ |
||||
|
EntityType.ASSET, |
||||
|
EntityType.DEVICE, |
||||
|
EntityType.ENTITY_VIEW, |
||||
|
EntityType.DASHBOARD, |
||||
|
EntityType.RULE_CHAIN |
||||
|
] |
||||
|
|
||||
|
export interface EdgeOverviewNode extends NavTreeNode { |
||||
|
data?: EdgeOverviewNodeData; |
||||
|
} |
||||
|
|
||||
|
export type EdgeOverviewNodeData = EdgeGroupNodeData | EntityNodeData; |
||||
|
|
||||
|
export interface EdgeGroupNodeData extends BaseEdgeOverviewNodeData { |
||||
|
entityType: EntityType; |
||||
|
entity: BaseData<HasId>; |
||||
|
} |
||||
|
|
||||
|
export interface EntityNodeData extends BaseEdgeOverviewNodeData { |
||||
|
entity: BaseData<HasId>; |
||||
|
} |
||||
|
|
||||
|
export interface BaseEdgeOverviewNodeData { |
||||
|
internalId: string; |
||||
|
} |
||||
Loading…
Reference in new issue