Implement entities hierarchy widget

This commit is contained in:
Igor Kulikov 2020-01-31 20:26:08 +02:00
parent d47371d8fd
commit 7261c75c61
26 changed files with 1594 additions and 26 deletions

File diff suppressed because one or more lines are too long

View File

@ -36,7 +36,8 @@
"node_modules/tooltipster/dist/css/tooltipster.bundle.min.css",
"node_modules/tooltipster/dist/css/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css",
"src/app/shared/components/json-form/react/json-form.scss",
"node_modules/rc-select/assets/index.css"
"node_modules/rc-select/assets/index.css",
"node_modules/jstree-bootstrap-theme/dist/themes/proton/style.min.css"
],
"stylePreprocessorOptions": {
"includePaths": [
@ -79,7 +80,8 @@
"node_modules/ace-builds/src-min/snippets/json.js",
"node_modules/ace-builds/src-min/snippets/java.js",
"node_modules/ace-builds/src-min/snippets/javascript.js",
"node_modules/systemjs/dist/system.js"
"node_modules/systemjs/dist/system.js",
"node_modules/jstree/dist/jstree.min.js"
],
"es5BrowserSupport": true,
"customWebpackConfig": {

View File

@ -3603,6 +3603,15 @@
"integrity": "sha512-B1Br8yE27obcYvFx5ECZswT/947aAFNb9lHqnkUOhtOfvJqaa6Axibo4T+5G6iQlUfjgSd8am9R/9j9UBfRlrw==",
"dev": true
},
"@types/jstree": {
"version": "3.3.39",
"resolved": "https://registry.npmjs.org/@types/jstree/-/jstree-3.3.39.tgz",
"integrity": "sha512-lUUl9NCRqziIXYTC0n6NUykQS0oII5tUhXq6/pXVZSbrbcxB0jr3i7Bpn/8v6f8BVA9su1/NF/DoJ8IrwKuzKw==",
"dev": true,
"requires": {
"@types/jquery": "*"
}
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -8685,6 +8694,22 @@
"jss": "10.0.0"
}
},
"jstree": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/jstree/-/jstree-3.3.8.tgz",
"integrity": "sha512-0/nhGxVLSGfGQyVg+q59ocqSEKWRDKHoA8wNrcOIvlzCCw19tzvcMNGJ19hf+U0b7fycABowkny7fQPcLgUwwA==",
"requires": {
"jquery": ">=1.9.1"
}
},
"jstree-bootstrap-theme": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/jstree-bootstrap-theme/-/jstree-bootstrap-theme-1.0.1.tgz",
"integrity": "sha1-fV7cc6hG6Np/lPV6HMXd7p2eq0s=",
"requires": {
"jquery": ">=1.9.1"
}
},
"jszip": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.2.tgz",

View File

@ -54,6 +54,8 @@
"jquery.terminal": "^2.9.0",
"js-beautify": "^1.10.2",
"json-schema-defaults": "^0.4.0",
"jstree": "^3.3.8",
"jstree-bootstrap-theme": "^1.0.1",
"material-design-icons": "^3.0.1",
"messageformat": "^2.3.0",
"moment": "^2.24.0",
@ -93,6 +95,7 @@
"@types/jasminewd2": "~2.0.8",
"@types/jquery": "^3.3.31",
"@types/js-beautify": "^1.8.1",
"@types/jstree": "^3.3.39",
"@types/node": "~12.12.17",
"@types/react": "^16.9.16",
"@types/react-dom": "^16.9.4",

View File

@ -51,7 +51,7 @@ export class AppComponent implements OnInit {
this.matIconRegistry.addSvgIconLiteral(
'alpha-a-circle-outline',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg><path d="M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,' +
'<svg viewBox="0 0 24 24"><path d="M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,' +
'1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 ' +
'0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A1' +
'0,10 0 0,1 2,12A10,10 0 0,1 12,2Z" /></svg>'

View File

@ -128,7 +128,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
if (errorResponse.error.refreshTokenPending || errorResponse.status === 401) {
if (errorResponse.error.refreshTokenPending || errorCode && errorCode === Constants.serverErrorCode.jwtTokenExpired) {
return this.refreshTokenAndRetry(req, next);
} else {
} else if (errorCode !== Constants.serverErrorCode.credentialsExpired) {
unhandled = true;
}
} else if (errorResponse.status === 429) {

View File

@ -91,5 +91,11 @@
opacity: 1;
}
}
ul.indicators {
pointer-events: none;
li {
pointer-events: all;
}
}
}
}

View File

@ -21,14 +21,22 @@
}
:host ::ng-deep {
.mat-tab-body-wrapper {
position: absolute;
top: 49px;
left: 0;
right: 0;
bottom: 0;
}
.mat-tab-label {
min-width: 40px;
tb-details-panel {
> .mat-content {
> .mat-tab-group {
> .mat-tab-body-wrapper {
position: absolute;
top: 49px;
left: 0;
right: 0;
bottom: 0;
}
> .mat-tab-header {
.mat-tab-label {
min-width: 40px;
}
}
}
}
}
}

View File

@ -0,0 +1,51 @@
<!--
Copyright © 2016-2019 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-entities-hierarchy 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>&nbsp;</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-entities-nav-tree-panel">
<tb-nav-tree
[loadNodes]="loadNodes"
[onNodeSelected]="onNodeSelected"
[onNodesInserted]="onNodesInserted"
[editCallbacks]="nodeEditCallbacks"
enableSearch="true"
[searchCallback]="searchCallback"
></tb-nav-tree>
</div>
</div>
</div>

View File

@ -0,0 +1,122 @@
/**
* Copyright © 2016-2019 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-entities-hierarchy {
mat-toolbar {
height: 60px;
max-height: 60px;
.mat-toolbar-tools {
height: 60px;
max-height: 60px;
}
}
}
}
:host {
.tb-entities-hierarchy {
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-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;
}
}
}
}
}
}
}

View File

@ -0,0 +1,490 @@
///
/// Copyright © 2016-2019 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 { forkJoin, fromEvent, Observable, of } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, mergeMap, tap } from 'rxjs/operators';
import { constructTableCssString } from '@home/components/widget/lib/table-widget.models';
import { Overlay } from '@angular/cdk/overlay';
import {
LoadNodesCallback,
NavTreeEditCallbacks,
NodeSearchCallback,
NodeSelectedCallback,
NodesInsertedCallback
} from '@shared/components/nav-tree.component';
import { BaseData } from '@shared/models/base-data';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
import { deepClone } from '@core/utils';
import {
defaultNodeIconFunction,
defaultNodeOpenedFunction,
defaultNodeRelationQueryFunction,
defaultNodesSortFunction,
EntitiesHierarchyWidgetSettings,
HierarchyNavTreeNode,
HierarchyNodeContext,
HierarchyNodeDatasource,
iconUrlHtml,
loadNodeCtxFunction,
materialIconHtml,
NodeDisabledFunction,
NodeHasChildrenFunction,
NodeIconFunction,
NodeOpenedFunction,
NodeRelationQueryFunction,
NodesSortFunction,
NodeTextFunction
} from '@home/components/widget/lib/entities-hierarchy-widget.models';
import { EntityService } from '@core/http/entity.service';
import { EntityRelationsQuery, EntitySearchDirection } from '@shared/models/relation.models';
import { EntityRelationService } from '@core/http/entity-relation.service';
import { ActionNotificationShow } from '@core/notification/notification.actions';
@Component({
selector: 'tb-entities-hierarchy-widget',
templateUrl: './entities-hierarchy-widget.component.html',
styleUrls: ['./entities-hierarchy-widget.component.scss']
})
export class EntitiesHierarchyWidgetComponent extends PageComponent implements OnInit, AfterViewInit {
@Input()
ctx: WidgetContext;
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
public toastTargetId = 'entities-hierarchy-' + this.utils.guid();
public textSearchMode = false;
public textSearch = null;
public nodeEditCallbacks: NavTreeEditCallbacks = {};
private settings: EntitiesHierarchyWidgetSettings;
private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription;
private datasources: Array<HierarchyNodeDatasource>;
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 searchAction: WidgetAction = {
name: 'action.search',
show: true,
icon: 'search',
onAction: () => {
this.enterFilterMode();
}
};
constructor(protected store: Store<AppState>,
private elementRef: ElementRef,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private utils: UtilsService,
private entityService: EntityService,
private entityRelationService: EntityRelationService) {
super(store);
}
ngOnInit(): void {
this.ctx.$scope.entitiesHierarchyWidget = 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.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 = 'entities-hierarchy-' + this.utils.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 = undefined;
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 tasks: Observable<HierarchyNavTreeNode>[] = [];
this.datasources.forEach((datasource) => {
tasks.push(this.datasourceToNode(datasource));
});
forkJoin(tasks).subscribe((nodes) => {
cb(this.prepareNodes(nodes));
this.updateNodeData(this.subscription.data);
});
} else {
if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.nodeCtx.entity.id.entityType !== 'function') {
const relationQuery = this.prepareNodeRelationQuery(node.data.nodeCtx);
this.entityRelationService.findByQuery(relationQuery, {ignoreErrors: true, ignoreLoading: true}).subscribe(
(entityRelations) => {
if (entityRelations.length) {
const tasks: Observable<HierarchyNavTreeNode>[] = [];
entityRelations.forEach((relation) => {
const targetId = relationQuery.parameters.direction === EntitySearchDirection.FROM ? relation.to : relation.from;
tasks.push(this.entityIdToNode(targetId.entityType as EntityType, targetId.id, node.data.datasource, node.data.nodeCtx));
});
forkJoin(tasks).subscribe((nodes) => {
cb(this.prepareNodes(nodes));
});
} else {
cb([]);
}
},
(error) => {
let errorText = 'Failed to get relations!';
if (error && error.status === 400) {
errorText = 'Invalid relations query returned by \'Node relations query function\'! Please check widget configuration!';
}
this.showError(errorText);
}
);
} else {
cb([]);
}
}
};
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, parent) => {
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 showError(errorText: string) {
this.store.dispatch(new ActionNotificationShow(
{
message: errorText,
type: 'error',
target: this.toastTargetId,
verticalPosition: 'bottom',
horizontalPosition: 'left'
}));
}
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, parentNodeCtx?: HierarchyNodeContext): Observable<HierarchyNavTreeNode> {
return this.resolveEntity(datasource).pipe(
map(entity => {
if (entity !== null) {
const node: HierarchyNavTreeNode = {
id: (++this.nodeIdCounter)+''
};
this.nodesMap[node.id] = node;
datasource.nodeId = node.id;
node.icon = false;
const nodeCtx: HierarchyNodeContext = {
parentNodeCtx,
entity,
data: {}
};
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;
} else {
return null;
}
})
);
}
private entityIdToNode(entityType: EntityType, entityId: string,
parentDatasource: HierarchyNodeDatasource,
parentNodeCtx: HierarchyNodeContext): Observable<HierarchyNavTreeNode> {
const datasource = {
dataKeys: parentDatasource.dataKeys,
type: DatasourceType.entity,
entityType,
entityId
} as HierarchyNodeDatasource;
return this.datasourceToNode(datasource, parentNodeCtx).pipe(
mergeMap((node) => {
if (node != null) {
const subscriptionOptions: WidgetSubscriptionOptions = {
type: widgetType.latest,
datasources: [datasource],
callbacks: {
onDataUpdated: subscription => {
this.updateNodeData(subscription.data);
}
}
};
return this.ctx.subscriptionApi.
createSubscription(subscriptionOptions, true).pipe(
map(() => node));
} else {
return of(node);
}
})
);
}
private resolveEntity(datasource: HierarchyNodeDatasource): Observable<BaseData<EntityId>> {
if (datasource.type === DatasourceType.function) {
const entity = {
id: {
entityType: 'function'
},
name: datasource.name
};
return of(entity as BaseData<EntityId>);
} else {
return this.entityService.getEntity(datasource.entityType, datasource.entityId, {ignoreLoading: true}).pipe(
catchError(err => of(null))
);
}
}
private prepareNodeRelationQuery(nodeCtx: HierarchyNodeContext): EntityRelationsQuery {
let relationQuery = this.nodeRelationQueryFunction(nodeCtx);
if (relationQuery && relationQuery === 'default') {
relationQuery = defaultNodeRelationQueryFunction(nodeCtx);
}
return relationQuery as EntityRelationsQuery;
}
}

View File

@ -0,0 +1,160 @@
///
/// Copyright © 2016-2019 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';
export interface EntitiesHierarchyWidgetSettings {
nodeRelationQueryFunction: string;
nodeHasChildrenFunction: string;
nodeOpenedFunction: string;
nodeDisabledFunction: string;
nodeIconFunction: string;
nodeTextFunction: string;
nodesSortFunction: string;
}
export interface HierarchyNodeContext {
parentNodeCtx?: HierarchyNodeContext;
entity: BaseData<EntityId>;
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 Function>(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+');">&nbsp;</div>';
}
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 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;
};

View File

@ -23,6 +23,7 @@ import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-t
import { AlarmStatusFilterPanelComponent } from '@home/components/widget/lib/alarm-status-filter-panel.component';
import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component';
import { EntitiesHierarchyWidgetComponent } from '@home/components/widget/lib/entities-hierarchy-widget.component';
@NgModule({
entryComponents: [
@ -35,7 +36,8 @@ import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/time
AlarmStatusFilterPanelComponent,
EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent,
TimeseriesTableWidgetComponent
TimeseriesTableWidgetComponent,
EntitiesHierarchyWidgetComponent
],
imports: [
CommonModule,
@ -45,7 +47,8 @@ import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/time
exports: [
EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent,
TimeseriesTableWidgetComponent
TimeseriesTableWidgetComponent,
EntitiesHierarchyWidgetComponent
]
})
export class WidgetComponentsModule { }

View File

@ -51,6 +51,16 @@ const routes: Routes = [
},
canActivate: [AuthGuard]
},
{
path: 'login/resetExpiredPassword',
component: ResetPasswordComponent,
data: {
title: 'login.reset-password',
module: 'public',
expiredPassword: true
},
canActivate: [AuthGuard]
},
{
path: 'login/createPassword',
component: CreatePasswordComponent,

View File

@ -21,6 +21,9 @@ import { Store } from '@ngrx/store';
import { AppState } from '../../../../core/core.state';
import { PageComponent } from '../../../../shared/components/page.component';
import { FormBuilder } from '@angular/forms';
import { HttpErrorResponse } from '@angular/common/http';
import { Constants } from '@shared/models/constants';
import { Router } from '@angular/router';
@Component({
selector: 'tb-login',
@ -36,7 +39,8 @@ export class LoginComponent extends PageComponent implements OnInit {
constructor(protected store: Store<AppState>,
private authService: AuthService,
public fb: FormBuilder) {
public fb: FormBuilder,
private router: Router) {
super(store);
}
@ -45,7 +49,16 @@ export class LoginComponent extends PageComponent implements OnInit {
login(): void {
if (this.loginFormGroup.valid) {
this.authService.login(this.loginFormGroup.value).subscribe();
this.authService.login(this.loginFormGroup.value).subscribe(
() => {},
(error: HttpErrorResponse) => {
if (error && error.error && error.error.errorCode) {
if (error.error.errorCode === Constants.serverErrorCode.credentialsExpired) {
this.router.navigateByUrl(`login/resetExpiredPassword?resetToken=${error.error.resetToken}`);
}
}
}
);
} else {
Object.keys(this.loginFormGroup.controls).forEach(field => {
const control = this.loginFormGroup.get(field);

View File

@ -20,6 +20,9 @@
<mat-card-title>
<span translate class="mat-headline">login.password-reset</span>
</mat-card-title>
<mat-card-subtitle>
<span *ngIf="isExpiredPassword" translate>login.expired-password-reset-message</span>
</mat-card-subtitle>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<span style="height: 4px;" *ngIf="!(isLoading$ | async)"></span>

View File

@ -34,6 +34,8 @@ import { map } from 'rxjs/operators';
})
export class ResetPasswordComponent extends PageComponent implements OnInit, OnDestroy {
isExpiredPassword: boolean;
resetToken = '';
sub: Subscription;
@ -51,6 +53,7 @@ export class ResetPasswordComponent extends PageComponent implements OnInit, OnD
}
ngOnInit() {
this.isExpiredPassword = this.route.snapshot.data.expiredPassword;
this.sub = this.route
.queryParams
.subscribe(params => {

View File

@ -0,0 +1,18 @@
<!--
Copyright © 2016-2019 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-nav-tree-container"></div>

View File

@ -0,0 +1,347 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-nav-tree-container {
padding: 15px;
font-family: Roboto, "Helvetica Neue", sans-serif;
&.jstree-proton {
.jstree-node,
.jstree-icon {
background-image: url("../../../assets/jstree/32px.png");
}
.jstree-last {
background: transparent;
}
.jstree-themeicon-custom {
background-image: none;
&.material-icons {
font-size: 18px;
}
}
.jstree-anchor {
font-size: 16px;
}
}
&.jstree-proton-small {
.jstree-node,
.jstree-icon {
background-image: url("../../../assets/jstree/32px.png");
}
.jstree-last {
background: transparent;
}
.jstree-themeicon-custom {
background-image: none;
&.material-icons {
font-size: 14px;
}
}
.jstree-anchor {
font-size: 14px;
}
}
&.jstree-proton-large {
.jstree-node,
.jstree-icon {
background-image: url("../../../assets/jstree/32px.png");
}
.jstree-last {
background: transparent;
}
.jstree-themeicon-custom {
background-image: none;
&.material-icons {
font-size: 24px;
}
}
.jstree-anchor {
font-size: 20px;
}
}
a {
border-bottom: none;
i.jstree-themeicon-custom {
&.tb-user-group {
&::before {
content: "account_circle";
}
}
&.tb-customer-group {
&::before {
content: "supervisor_account";
}
}
&.tb-asset-group {
&::before {
content: "domain";
}
}
&.tb-device-group {
&::before {
content: "devices_other";
}
}
&.tb-entity-view-group {
&::before {
content: "view_quilt";
}
}
&.tb-dashboard-group {
&::before {
content: "dashboard";
}
}
&.tb-customer {
&::before {
content: "supervisor_account";
}
}
}
}
}
@media (max-width: 768px) {
.tb-nav-tree-container {
&.jstree-proton-responsive {
.jstree-node,
.jstree-icon,
.jstree-node > .jstree-ocl,
.jstree-themeicon,
.jstree-checkbox {
background-image: url("../../../assets/jstree/40px.png");
background-size: 120px 240px;
}
.jstree-container-ul {
overflow: visible;
}
.jstree-themeicon-custom {
background-color: transparent;
background-image: none;
background-position: 0 0;
&.material-icons {
margin: 0;
font-size: 24px;
}
}
.jstree-node,
.jstree-leaf > .jstree-ocl {
background: 0 0;
}
.jstree-node {
min-width: 40px;
min-height: 40px;
margin-left: 40px;
line-height: 40px;
white-space: nowrap;
background-repeat: repeat-y;
background-position: -80px 0;
}
.jstree-last {
background: 0 0;
}
.jstree-anchor {
height: 40px;
font-size: 1.1em;
font-weight: 700;
line-height: 40px;
text-shadow: 1px 1px #fff;
}
.jstree-icon,
.jstree-icon:empty {
width: 40px;
height: 40px;
line-height: 40px;
}
> {
.jstree-container-ul > .jstree-node {
margin-right: 0;
margin-left: 0;
}
}
.jstree-ocl,
.jstree-themeicon,
.jstree-checkbox {
background-size: 120px 240px;
}
.jstree-leaf > .jstree-ocl {
background: 0 0;
background-position: -40px -120px;
}
.jstree-last > .jstree-ocl {
background-position: -40px -160px;
}
.jstree-open > .jstree-ocl {
background-position: 0 0 !important;
}
.jstree-closed > .jstree-ocl {
background-position: 0 -40px !important;
}
.jstree-themeicon {
background-position: -40px -40px;
}
.jstree-checkbox,
.jstree-checkbox:hover {
background-position: -40px -80px;
}
&.jstree-checkbox-selection {
.jstree-clicked > .jstree-checkbox,
.jstree-clicked > .jstree-checkbox:hover {
background-position: 0 -80px;
}
}
.jstree-checked > .jstree-checkbox,
.jstree-checked > .jstree-checkbox:hover {
background-position: 0 -80px;
}
.jstree-anchor > .jstree-undetermined,
.jstree-anchor > .jstree-undetermined:hover {
background-position: 0 -120px;
}
.jstree-striped {
background: 0 0;
}
.jstree-wholerow {
height: 40px;
background: #ebebeb;
border-top: 1px solid rgba(255, 255, 255, .7);
border-bottom: 1px solid rgba(64, 64, 64, .2);
}
.jstree-wholerow-hovered {
background: #e7f4f9;
}
.jstree-wholerow-clicked {
background: #beebff;
}
.jstree-children {
.jstree-last > .jstree-wholerow {
box-shadow: inset 0 -6px 3px -5px #666;
}
.jstree-open > .jstree-wholerow {
border-top: 0;
box-shadow: inset 0 6px 3px -5px #666;
}
.jstree-open + .jstree-open {
box-shadow: none;
}
}
&.jstree-rtl {
.jstree-node {
margin-right: 40px;
margin-left: 0;
}
.jstree-container-ul > .jstree-node {
margin-right: 0;
}
.jstree-closed > .jstree-ocl {
background-position: -40px 0 !important;
}
}
}
}
}
.tb-nav-tree .mat-button.tb-active {
font-weight: 500;
background-color: rgba(255, 255, 255, .15);
}
.tb-nav-tree,
.tb-nav-tree ul {
margin-top: 0;
list-style: none;
&:first-child {
padding: 0;
}
li {
.mat-button {
width: 100%;
max-height: 40px;
padding: 0 16px;
margin: 0;
overflow: hidden;
line-height: 40px;
color: inherit;
text-align: left;
text-decoration: none;
text-overflow: ellipsis;
text-transform: none;
text-rendering: optimizeLegibility;
white-space: nowrap;
cursor: pointer;
border-radius: 0;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}

View File

@ -0,0 +1,272 @@
///
/// Copyright © 2016-2019 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 { Component, ElementRef, Input, NgZone, OnInit, ViewEncapsulation } from '@angular/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { deepClone } from '@core/utils';
export interface NavTreeNodeState {
disabled?: boolean;
opened?: boolean;
loaded?: boolean;
}
export interface NavTreeNode {
id: string;
icon?: boolean;
text?: string;
state?: NavTreeNodeState;
children?: NavTreeNode[] | boolean;
data?: any;
}
export interface NavTreeEditCallbacks {
selectNode?: (id: string) => void;
deselectAll?: () => void;
getNode?: (id: string) => NavTreeNode;
getParentNodeId?: (id: string) => string;
openNode?: (id: string, cb?: () => void) => void;
nodeIsOpen?: (id: string) => boolean;
nodeIsLoaded?: (id: string) => boolean;
refreshNode?: (id: string) => void;
updateNode?: (id: string, newName: string) => void;
createNode?: (parentId: string, node: NavTreeNode, pos: number) => void;
deleteNode?: (id: string) => void;
disableNode?: (id: string) => void;
enableNode?: (id: string) => void;
setNodeHasChildren?: (id: string, hasChildren: boolean) => void;
search?: (searchText: string) => void;
clearSearch?: () => void;
}
export type NodesCallback = (nodes: NavTreeNode[]) => void;
export type LoadNodesCallback = (node: NavTreeNode, cb: NodesCallback) => void;
export type NodeSearchCallback = (searchText: string, node: NavTreeNode) => boolean;
export type NodeSelectedCallback = (node: NavTreeNode, event: Event) => void;
export type NodesInsertedCallback = (nodes: string[], parent: string) => void;
@Component({
selector: 'tb-nav-tree',
templateUrl: './nav-tree.component.html',
styleUrls: ['./nav-tree.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class NavTreeComponent implements OnInit {
private enableSearchValue: boolean;
get enableSearch(): boolean {
return this.enableSearchValue;
}
@Input()
set enableSearch(value: boolean) {
this.enableSearchValue = coerceBooleanProperty(value);
}
@Input()
private loadNodes: LoadNodesCallback;
@Input()
private searchCallback: NodeSearchCallback;
@Input()
private onNodeSelected: NodeSelectedCallback;
@Input()
private onNodesInserted: NodesInsertedCallback;
@Input()
private editCallbacks: NavTreeEditCallbacks;
private treeElement: JSTree;
constructor(private elementRef: ElementRef<HTMLElement>,
private ngZone: NgZone) {
}
ngOnInit(): void {
this.initTree();
}
private initTree() {
const loadNodes: LoadNodesCallback = (node, cb) => {
const outCb = (_nodes: NavTreeNode[]) => {
const copied: NavTreeNode[] = [];
if (_nodes) {
_nodes.forEach((n) => {
copied.push(deepClone(n, ['data']));
});
}
cb(copied);
};
this.ngZone.runOutsideAngular(() => {
this.loadNodes(node, outCb);
});
};
const config: JSTreeStaticDefaults = {
core: {
worker: false,
multiple: false,
check_callback: true,
themes: { name: 'proton', responsive: true },
data: loadNodes,
error: () => {
console.error('Unexpected jstree error!');
}
},
plugins: []
};
if (this.enableSearch) {
config.plugins.push('search');
config.search = {
ajax: false,
fuzzy: false,
close_opened_onclear: true,
case_sensitive: false,
show_only_matches: true,
show_only_matches_children: false,
search_leaves_only: false,
search_callback: this.searchCallback
};
}
this.treeElement = $('.tb-nav-tree-container', this.elementRef.nativeElement).jstree(config);
this.treeElement.on('changed.jstree', (e, data) => {
const node: NavTreeNode = data.instance.get_selected(true)[0];
if (this.onNodeSelected) {
this.onNodeSelected(node, e);
}
});
this.treeElement.on('model.jstree', (e, data) => {
if (this.onNodesInserted) {
this.onNodesInserted(data.nodes, data.parent);
}
});
if (this.editCallbacks) {
this.editCallbacks.selectNode = id => {
const node: NavTreeNode = this.treeElement.jstree('get_node', id);
if (node) {
this.treeElement.jstree('deselect_all', true);
this.treeElement.jstree('select_node', node);
}
};
this.editCallbacks.deselectAll = () => {
this.treeElement.jstree('deselect_all');
};
this.editCallbacks.getNode = (id) => {
const node: NavTreeNode = this.treeElement.jstree('get_node', id);
return node;
};
this.editCallbacks.getParentNodeId = (id) => {
const node: NavTreeNode = this.treeElement.jstree('get_node', id);
if (node) {
return this.treeElement.jstree('get_parent', node);
}
};
this.editCallbacks.openNode = (id, cb) => {
const node: NavTreeNode = this.treeElement.jstree('get_node', id);
if (node) {
this.treeElement.jstree('open_node', node, cb);
}
};
this.editCallbacks.nodeIsOpen = (id) => {
const node: NavTreeNode = this.treeElement.jstree('get_node', id);
if (node) {
return this.treeElement.jstree('is_open', node);
} else {
return true;
}
};
this.editCallbacks.nodeIsLoaded = (id) => {
const node: NavTreeNode = this.treeElement.jstree('get_node', id);
if (node) {
return this.treeElement.jstree('is_loaded', node);
} else {
return true;
}
};
this.editCallbacks.refreshNode = (id) => {
if (id === '#') {
this.treeElement.jstree('refresh');
this.treeElement.jstree('redraw');
} else {
const node: NavTreeNode = this.treeElement.jstree('get_node', id);
if (node) {
const opened = this.treeElement.jstree('is_open', node);
this.treeElement.jstree('refresh_node', node);
this.treeElement.jstree('redraw');
if (node.children && opened/* && !node.children.length*/) {
this.treeElement.jstree('open_node', node);
}
}
}
};
this.editCallbacks.updateNode = (id, newName) => {
const node: NavTreeNode = this.treeElement.jstree('get_node', id);
if (node) {
this.treeElement.jstree('rename_node', node, newName);
}
};
this.editCallbacks.createNode = (parentId, node, pos) => {
const parentNode: NavTreeNode = this.treeElement.jstree('get_node', parentId);
if (parentNode) {
this.treeElement.jstree('create_node', parentNode, node, pos);
}
};
this.editCallbacks.deleteNode = (id) => {
const node: NavTreeNode = this.treeElement.jstree('get_node', id);
if (node) {
this.treeElement.jstree('delete_node', node);
}
};
this.editCallbacks.disableNode = (id) => {
const node: NavTreeNode = this.treeElement.jstree('get_node', id);
if (node) {
this.treeElement.jstree('disable_node', node);
}
};
this.editCallbacks.enableNode = (id) => {
const node: NavTreeNode = this.treeElement.jstree('get_node', id);
if (node) {
this.treeElement.jstree('enable_node', node);
}
};
this.editCallbacks.setNodeHasChildren = (id, hasChildren) => {
const node: NavTreeNode = this.treeElement.jstree('get_node', id);
if (node) {
if (!node.children || (Array.isArray(node.children) && !node.children.length)) {
node.children = hasChildren;
node.state.loaded = !hasChildren;
node.state.opened = false;
this.treeElement.jstree('_node_changed', node.id);
this.treeElement.jstree('redraw');
}
}
};
this.editCallbacks.search = (searchText) => {
this.treeElement.jstree('search', searchText);
};
this.editCallbacks.clearSearch = () => {
this.treeElement.jstree('clear_search');
};
}
}
}

View File

@ -20,6 +20,7 @@ export const Constants = {
authentication: 10,
jwtTokenExpired: 11,
tenantTrialExpired: 12,
credentialsExpired: 15,
permissionDenied: 20,
invalidArguments: 30,
badRequestParams: 31,

View File

@ -120,6 +120,7 @@ import { JsonContentComponent } from './components/json-content.component';
import { KeyValMapComponent } from './components/kv-map.component';
import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component';
import { TbHotkeysDirective } from '@shared/components/hotkeys.directive';
import { NavTreeComponent } from '@shared/components/nav-tree.component';
@NgModule({
providers: [
@ -198,6 +199,7 @@ import { TbHotkeysDirective } from '@shared/components/hotkeys.directive';
FileInputComponent,
MessageTypeAutocompleteComponent,
KeyValMapComponent,
NavTreeComponent,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
@ -346,6 +348,7 @@ import { TbHotkeysDirective } from '@shared/components/hotkeys.directive';
FileInputComponent,
MessageTypeAutocompleteComponent,
KeyValMapComponent,
NavTreeComponent,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,

View File

@ -21,12 +21,10 @@
min-height: #{$size}px;
font-size: #{$size}px;
line-height: #{$size}px;
/* svg {
width: 24px;
height: 24px;
transform: scale($size/24);
transform-origin: top;
}*/
svg {
width: #{$size}px;
height: #{$size}px;
}
}
@mixin tb-mat-icon-button-size($size) {

View File

@ -2,7 +2,7 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"types": ["node", "jquery", "flot", "tooltipster", "tinycolor2", "js-beautify", "react", "react-dom"]
"types": ["node", "jquery", "flot", "tooltipster", "tinycolor2", "js-beautify", "react", "react-dom", "jstree"]
},
"exclude": [
"test.ts",

View File

@ -0,0 +1,29 @@
///
/// Copyright © 2016-2019 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.
///
interface JSTreeEventData {
instance: JSTree;
}
interface JSTreeModelEventData extends JSTreeEventData {
nodes: string[];
parent: string;
}
interface JQuery {
on(events: 'changed.jstree', handler: (e: Event, data: JSTreeEventData) => void): this;
on(events: 'model.jstree', handler: (e: Event, data: JSTreeModelEventData) => void): this;
}

View File

@ -18,6 +18,7 @@
"src/typings/rawloader.typings.d.ts",
"src/typings/jquery.typings.d.ts",
"src/typings/jquery.flot.typings.d.ts",
"src/typings/jquery.jstree.typings.d.ts",
"src/typings/split.js.typings.d.ts"
],
"paths": {