Implement entities hierarchy widget
This commit is contained in:
parent
d47371d8fd
commit
7261c75c61
File diff suppressed because one or more lines are too long
@ -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": {
|
||||
|
||||
25
ui-ngx/package-lock.json
generated
25
ui-ngx/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>'
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -91,5 +91,11 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
ul.indicators {
|
||||
pointer-events: none;
|
||||
li {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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> </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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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+');"> </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;
|
||||
};
|
||||
@ -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 { }
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 => {
|
||||
|
||||
18
ui-ngx/src/app/shared/components/nav-tree.component.html
Normal file
18
ui-ngx/src/app/shared/components/nav-tree.component.html
Normal 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>
|
||||
347
ui-ngx/src/app/shared/components/nav-tree.component.scss
Normal file
347
ui-ngx/src/app/shared/components/nav-tree.component.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
272
ui-ngx/src/app/shared/components/nav-tree.component.ts
Normal file
272
ui-ngx/src/app/shared/components/nav-tree.component.ts
Normal 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');
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,7 @@ export const Constants = {
|
||||
authentication: 10,
|
||||
jwtTokenExpired: 11,
|
||||
tenantTrialExpired: 12,
|
||||
credentialsExpired: 15,
|
||||
permissionDenied: 20,
|
||||
invalidArguments: 30,
|
||||
badRequestParams: 31,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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",
|
||||
|
||||
29
ui-ngx/src/typings/jquery.jstree.typings.d.ts
vendored
Normal file
29
ui-ngx/src/typings/jquery.jstree.typings.d.ts
vendored
Normal 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;
|
||||
}
|
||||
@ -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": {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user