Edge widgets with empty datasource
This commit is contained in:
parent
19c04f3387
commit
58c580ecaf
File diff suppressed because one or more lines are too long
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -1,51 +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.
|
||||
|
||||
-->
|
||||
<div class="tb-edge-instances-overview tb-absolute-fill" tb-toast toastTarget="{{ toastTargetId }}">
|
||||
<div fxFlex fxLayout="column" class="tb-absolute-fill">
|
||||
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode">
|
||||
<div class="mat-toolbar-tools">
|
||||
<button mat-button mat-icon-button
|
||||
matTooltip="{{ 'action.search' | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon>search</mat-icon>
|
||||
</button>
|
||||
<mat-form-field fxFlex>
|
||||
<mat-label> </mat-label>
|
||||
<input #searchInput matInput
|
||||
[(ngModel)]="textSearch"
|
||||
placeholder="{{ 'entity.search' | translate }}"/>
|
||||
</mat-form-field>
|
||||
<button mat-button mat-icon-button (click)="exitFilterMode()"
|
||||
matTooltip="{{ 'action.close' | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
<div fxFlex class="tb-edges-nav-tree-panel">
|
||||
<tb-nav-tree
|
||||
[loadNodes]="loadNodes"
|
||||
[onNodeSelected]="onNodeSelected"
|
||||
[onNodesInserted]="onNodesInserted"
|
||||
[editCallbacks]="nodeEditCallbacks"
|
||||
enableSearch="true"
|
||||
[searchCallback]="searchCallback"
|
||||
></tb-nav-tree>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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<HierarchyNodeDatasource>;
|
||||
private data: Array<Array<DatasourceData>>;
|
||||
|
||||
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<AppState>,
|
||||
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<HierarchyNodeDatasource>;
|
||||
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<DatasourceData>) {
|
||||
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<Edge>): 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;
|
||||
}
|
||||
}
|
||||
@ -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<EntityId>;
|
||||
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<F extends (...args: any[]) => 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 '<mat-icon class="node-icon material-icons" role="img" aria-hidden="false">' + materialIcon + '</mat-icon>';
|
||||
}
|
||||
|
||||
export function iconUrlHtml(iconUrl: string): string {
|
||||
return '<div class="node-icon" style="background-image: url(' + iconUrl + ');"> </div>';
|
||||
}
|
||||
|
||||
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 '<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 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';
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user