RuleChain page

This commit is contained in:
Igor Kulikov 2019-12-12 19:55:17 +02:00
parent e85c47aebf
commit cf1684572b
20 changed files with 3543 additions and 617 deletions

View File

@ -1407,14 +1407,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -1429,20 +1427,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -1559,8 +1554,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -1572,7 +1566,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -1587,7 +1580,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -1699,8 +1691,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -1712,7 +1703,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -1834,7 +1824,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",

View File

@ -33,7 +33,10 @@
"styles": [
"src/styles.scss",
"node_modules/jquery.terminal/css/jquery.terminal.min.css",
"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",
"src/app/modules/home/pages/rulechain/rulechain-page.tooltipster.scss",
"node_modules/rc-select/assets/index.css"
],
"stylePreprocessorOptions": {
@ -54,6 +57,7 @@
"node_modules/flot/src/plugins/jquery.flot.stack.js",
"node_modules/flot.curvedlines/curvedLines.js",
"node_modules/tinycolor2/dist/tinycolor-min.js",
"node_modules/tooltipster/dist/js/tooltipster.bundle.min.js",
"node_modules/split.js/dist/split.js",
"node_modules/js-beautify/js/lib/beautify.js",
"node_modules/js-beautify/js/lib/beautify-css.js",

3480
ui-ngx/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,52 +12,52 @@
},
"private": true,
"dependencies": {
"@angular/animations": "~8.2.11",
"@angular/animations": "~8.2.14",
"@angular/cdk": "~8.2.3",
"@angular/common": "~8.2.11",
"@angular/compiler": "~8.2.11",
"@angular/core": "~8.2.11",
"@angular/flex-layout": "^8.0.0-beta.26",
"@angular/forms": "~8.2.11",
"@angular/common": "~8.2.14",
"@angular/compiler": "~8.2.14",
"@angular/core": "~8.2.14",
"@angular/flex-layout": "^8.0.0-beta.27",
"@angular/forms": "~8.2.14",
"@angular/material": "^8.2.3",
"@angular/platform-browser": "~8.2.11",
"@angular/platform-browser-dynamic": "~8.2.11",
"@angular/router": "~8.2.11",
"@auth0/angular-jwt": "^3.0.0",
"@date-io/date-fns": "^1.3.11",
"@angular/platform-browser": "~8.2.14",
"@angular/platform-browser-dynamic": "~8.2.14",
"@angular/router": "~8.2.14",
"@auth0/angular-jwt": "^3.0.1",
"@date-io/date-fns": "^1.3.13",
"@flowjs/flow.js": "^2.13.2",
"@flowjs/ngx-flow": "^0.4.3",
"@mat-datetimepicker/core": "^2.0.1",
"@material-ui/core": "^4.5.1",
"@material-ui/core": "^4.7.2",
"@material-ui/icons": "^4.5.1",
"@material-ui/pickers": "^3.2.7",
"@ngrx/effects": "^8.2.0",
"@ngrx/store": "^8.2.0",
"@ngrx/store-devtools": "^8.2.0",
"@ngx-share/core": "^7.1.2",
"@material-ui/pickers": "^3.2.8",
"@ngrx/effects": "^8.5.2",
"@ngrx/store": "^8.5.2",
"@ngrx/store-devtools": "^8.5.2",
"@ngx-share/core": "^7.1.4",
"@ngx-translate/core": "^11.0.1",
"@ngx-translate/http-loader": "^4.0.0",
"ace-builds": "^1.4.5",
"angular-gridster2": "^8.1.0",
"ace-builds": "^1.4.7",
"angular-gridster2": "^8.2.0",
"angular2-hotkeys": "^2.1.5",
"base64-js": "^1.3.1",
"compass-sass-mixins": "^0.12.7",
"core-js": "^3.1.4",
"date-fns": "2.1.0",
"deep-equal": "^1.0.1",
"core-js": "^3.5.0",
"date-fns": "2.8.1",
"deep-equal": "^1.1.1",
"flot": "git://github.com/thingsboard/flot.git#0.9-work",
"flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master",
"font-awesome": "^4.7.0",
"hammerjs": "^2.0.8",
"javascript-detect-element-resize": "^0.5.3",
"jquery": "^3.4.1",
"jquery.terminal": "^2.8.0",
"jquery.terminal": "^2.9.0",
"js-beautify": "^1.10.2",
"json-schema-defaults": "^0.4.0",
"material-design-icons": "^3.0.1",
"messageformat": "^2.3.0",
"moment": "^2.24.0",
"ngx-clipboard": "^12.2.0",
"ngx-clipboard": "^12.3.0",
"ngx-color-picker": "^8.2.0",
"ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master",
"ngx-hm-carousel": "^1.7.2",
@ -65,50 +65,55 @@
"objectpath": "^1.2.2",
"prop-types": "^15.7.2",
"rc-select": "^9.2.1",
"react": "^16.10.2",
"react": "^16.12.0",
"react-ace": "^8.0.0",
"react-dom": "^16.10.2",
"react-dropzone": "^10.1.10",
"react-dom": "^16.12.0",
"react-dropzone": "^10.2.1",
"reactcss": "^1.2.3",
"rxjs": "~6.5.2",
"rxjs": "~6.5.3",
"schema-inspector": "^1.6.8",
"screenfull": "^4.2.1",
"screenfull": "^5.0.0",
"split.js": "^1.5.11",
"tinycolor2": "^1.4.1",
"tooltipster": "^4.2.7",
"tslib": "^1.10.0",
"tv4": "^1.3.0",
"typeface-roboto": "^0.0.75",
"zone.js": "~0.9.1"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^8.2.0",
"@angular-devkit/build-angular": "^0.802.0",
"@angular/cli": "~8.2.2",
"@angular/compiler-cli": "~8.2.11",
"@angular/language-service": "~8.2.11",
"@angular-builders/custom-webpack": "^8.4.1",
"@angular-devkit/build-angular": "^0.803.20",
"@angular/cli": "~8.3.20",
"@angular/compiler-cli": "~8.2.14",
"@angular/language-service": "~8.2.14",
"@types/flot": "0.0.31",
"@types/jasmine": "~3.4.0",
"@types/jasminewd2": "~2.0.6",
"@types/jasmine": "~3.5.0",
"@types/jasminewd2": "~2.0.8",
"@types/jquery": "^3.3.31",
"@types/js-beautify": "^1.8.1",
"@types/node": "~10.14.15",
"@types/react": "^16.9.9",
"@types/react-dom": "^16.9.2",
"@types/node": "~12.12.17",
"@types/react": "^16.9.16",
"@types/react-dom": "^16.9.4",
"@types/tinycolor2": "^1.4.2",
"codelyzer": "~5.1.0",
"compression-webpack-plugin": "^3.0.0",
"directory-tree": "^2.2.3",
"jasmine-core": "~3.4.0",
"@types/tooltipster": "0.0.29",
"codelyzer": "~5.2.0",
"compression-webpack-plugin": "^3.0.1",
"directory-tree": "^2.2.4",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.2.0",
"karma-chrome-launcher": "~3.0.0",
"karma-coverage-istanbul-reporter": "~2.1.0",
"karma": "~4.4.1",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.1.1",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.2",
"ngrx-store-freeze": "^0.2.4",
"protractor": "~5.4.2",
"ts-node": "~8.3.0",
"tslint": "~5.18.0",
"ts-node": "~8.5.4",
"tslint": "~5.20.1",
"typescript": "~3.5.3"
},
"resolutions": {
"serialize-javascript": "^2.1.1"
}
}

View File

@ -28,7 +28,7 @@ import { alarmFields } from '@shared/models/alarm.models';
import { materialColors } from '@app/shared/models/material.models';
import { WidgetInfo } from '@home/models/widget-component.models';
import jsonSchemaDefaults from 'json-schema-defaults';
import * as materialIconsCodepoints from '!raw-loader!material-design-icons/iconfont/codepoints';
import materialIconsCodepoints from '!raw-loader!material-design-icons/iconfont/codepoints';
import { Observable, of, ReplaySubject } from 'rxjs';
const varsRegex = /\$\{([^}]*)\}/g;

View File

@ -30,9 +30,9 @@ import { entityTypeTranslations } from '@shared/models/entity-type.models';
import { UtilsService } from '@core/services/utils.service';
import { deepClone, isUndefined } from '@core/utils';
import * as customSampleJs from '!raw-loader!./custom-sample-js.raw';
import * as customSampleCss from '!raw-loader!./custom-sample-css.raw';
import * as customSampleHtml from '!raw-loader!./custom-sample-html.raw';
import customSampleJs from '!raw-loader!./custom-sample-js.raw';
import customSampleCss from '!raw-loader!./custom-sample-css.raw';
import customSampleHtml from '!raw-loader!./custom-sample-html.raw';
export interface WidgetActionCallbacks {
fetchDashboardStates: (query: string) => Array<string>;

View File

@ -15,7 +15,7 @@
///
import { PageComponent } from '@shared/components/page.component';
import { Input, OnDestroy, OnInit } from '@angular/core';
import { Inject, Input, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { WidgetContext, IDynamicWidgetComponent } from '@home/models/widget-component.models';
@ -38,8 +38,8 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
[key: string]: any;
constructor(public raf: RafService,
protected store: Store<AppState>) {
constructor(@Inject(RafService) public raf: RafService,
@Inject(Store) protected store: Store<AppState>) {
super(store);
}

View File

@ -35,24 +35,48 @@
<mat-sidenav-content>
<div fxLayout="column" role="main" style="height: 100%;">
<mat-toolbar fxLayout="row" color="primary" class="mat-elevation-z1 tb-primary-toolbar">
<button [fxShow]="!forceFullscreen" mat-button mat-icon-button id="main" fxHide.gt-sm (click)="sidenav.toggle()">
<button [fxShow]="!forceFullscreen" mat-button mat-icon-button id="main"
[ngClass]="{'tb-invisible': displaySearchMode()}"
fxHide.gt-sm (click)="sidenav.toggle()">
<mat-icon class="material-icons">menu</mat-icon>
</button>
<button [fxShow]="forceFullscreen" mat-button mat-icon-button (click)="goBack()">
<button [fxShow]="forceFullscreen" mat-button mat-icon-button
[ngClass]="{'tb-invisible': displaySearchMode()}"
(click)="goBack()">
<mat-icon class="material-icons">arrow_back</mat-icon>
</button>
<div fxFlex tb-breadcrumb [activeComponent]="activeComponent" class="mat-toolbar-tools">
<button mat-button mat-icon-button
[ngClass]="{'tb-invisible': !displaySearchMode()}"
(click)="closeSearch()">
<mat-icon class="material-icons">arrow_back</mat-icon>
</button>
<div [fxShow]="!displaySearchMode()"
fxFlex tb-breadcrumb [activeComponent]="activeComponent" class="mat-toolbar-tools">
</div>
<button *ngIf="fullscreenEnabled" mat-button mat-icon-button fxHide.xs fxHide.sm (click)="toggleFullscreen()">
<div [fxShow]="displaySearchMode()" fxFlex fxLayout="row" class="tb-dark">
<mat-form-field fxFlex floatLabel="always">
<mat-label></mat-label>
<input #searchInput matInput
[(ngModel)]="searchText"
placeholder="{{ 'common.enter-search' | translate }}"/>
</mat-form-field>
</div>
<button [fxShow]="searchEnabled"
mat-button mat-icon-button
(click)="openSearch()">
<mat-icon class="material-icons">search</mat-icon>
</button>
<button *ngIf="fullscreenEnabled" [fxShow]="!displaySearchMode()"
mat-button mat-icon-button fxHide.xs fxHide.sm (click)="toggleFullscreen()">
<mat-icon class="material-icons">{{ isFullscreen() ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
<tb-user-menu [displayUserInfo]="true"></tb-user-menu>
<tb-user-menu [displayUserInfo]="!displaySearchMode()"></tb-user-menu>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; margin-bottom: -4px; width: 100%;" mode="indeterminate"
*ngIf="isLoading$ | async">
</mat-progress-bar>
<div fxFlex fxLayout="column" tb-toast class="tb-main-content">
<router-outlet (activate)="activeComponent = $event;"></router-outlet>
<router-outlet (activate)="activeComponentChanged($event)"></router-outlet>
</div>
</div>
</mat-sidenav-content>

View File

@ -17,6 +17,11 @@
display: flex;
width: 100%;
height: 100%;
.tb-invisible {
display: none !important;
}
mat-sidenav-container {
flex: 1;
}

View File

@ -14,10 +14,10 @@
/// limitations under the License.
///
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
import { Observable } from 'rxjs';
import { AfterViewInit, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import { fromEvent, Observable } from 'rxjs';
import { select, Store } from '@ngrx/store';
import { map, mergeMap, take } from 'rxjs/operators';
import { debounceTime, distinctUntilChanged, map, mergeMap, take, tap } from 'rxjs/operators';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { User } from '@shared/models/user.model';
@ -34,19 +34,21 @@ import * as screenfull from 'screenfull';
import { MatSidenav } from '@angular/material';
import { AuthState } from '@core/auth/auth.models';
import { WINDOW } from '@core/services/window.service';
import { ISearchableComponent, instanceOfSearchableComponent } from '@home/models/searchable-component.models';
@Component({
selector: 'tb-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent extends PageComponent implements OnInit {
export class HomeComponent extends PageComponent implements AfterViewInit, OnInit {
authState: AuthState = getCurrentAuthState(this.store);
forceFullscreen = this.authState.forceFullscreen;
activeComponent: any;
searchableComponent: ISearchableComponent;
sidenavMode = 'side';
sidenavOpened = true;
@ -56,6 +58,8 @@ export class HomeComponent extends PageComponent implements OnInit {
@ViewChild('sidenav', {static: false})
sidenav: MatSidenav;
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
// @ts-ignore
fullscreenEnabled = screenfull.enabled;
@ -63,6 +67,10 @@ export class HomeComponent extends PageComponent implements OnInit {
userDetails$: Observable<User>;
userDetailsString: Observable<string>;
searchEnabled = false;
showSearch = false;
searchText = '';
constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window,
private authService: AuthService,
@ -98,6 +106,18 @@ export class HomeComponent extends PageComponent implements OnInit {
);
}
ngAfterViewInit() {
fromEvent(this.searchInputField.nativeElement, 'keyup')
.pipe(
debounceTime(150),
distinctUntilChanged(),
tap(() => {
this.searchTextUpdated();
})
)
.subscribe();
}
sidenavClicked() {
if (this.sidenavMode === 'over') {
this.sidenav.toggle();
@ -120,4 +140,47 @@ export class HomeComponent extends PageComponent implements OnInit {
goBack() {
this.window.history.back();
}
activeComponentChanged(activeComponent: any) {
this.showSearch = false;
this.searchText = '';
this.activeComponent = activeComponent;
if (this.activeComponent && instanceOfSearchableComponent(this.activeComponent)) {
this.searchEnabled = true;
this.searchableComponent = this.activeComponent;
} else {
this.searchEnabled = false;
this.searchableComponent = null;
}
}
displaySearchMode(): boolean {
return this.searchEnabled && this.showSearch;
}
openSearch() {
if (this.searchEnabled) {
this.showSearch = true;
setTimeout(() => {
this.searchInputField.nativeElement.focus();
this.searchInputField.nativeElement.setSelectionRange(0, 0);
}, 10);
}
}
closeSearch() {
if (this.searchEnabled) {
this.showSearch = false;
if (this.searchText.length) {
this.searchText = '';
this.searchTextUpdated();
}
}
}
private searchTextUpdated() {
if (this.searchableComponent) {
this.searchableComponent.onSearchTextUpdated(this.searchText);
}
}
}

View File

@ -0,0 +1,23 @@
///
/// 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.
///
export interface ISearchableComponent {
onSearchTextUpdated(searchText: string);
}
export function instanceOfSearchableComponent(object: any): object is ISearchableComponent {
return 'onSearchTextUpdated' in object;
}

View File

@ -30,7 +30,7 @@
</button>
</section>
<mat-drawer-container style="width: 100%;">
<mat-drawer class="tb-rulechain-library"
<mat-drawer class="tb-rulechain-library mat-elevation-z4"
disableClose="true"
mode="side"
[opened]="isLibraryOpen"
@ -45,13 +45,13 @@
</button>
<mat-form-field fxFlex floatLabel="always">
<mat-label></mat-label>
<input matInput
[(ngModel)]="ruleNodeSearch"
<input #ruleNodeSearchInput matInput
[(ngModel)]="ruleNodeTypeSearch"
placeholder="{{'rulenode.search' | translate}}"/>
</mat-form-field>
<button mat-button mat-icon-button class="tb-small"
[fxShow]="ruleNodeSearch !== ''"
(click)="ruleNodeSearch = ''"
[fxShow]="ruleNodeTypeSearch !== ''"
(click)="ruleNodeTypeSearch = ''; updateRuleChainLibrary()"
matTooltip="{{'action.clear-search' | translate}}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
@ -64,17 +64,43 @@
</button>
</div>
</mat-toolbar>
<div class="tb-rulechain-library-panel-group">
<mat-expansion-panel #ruleNodeTypeExpansionPanels
class="mat-elevation-z2"
[expanded]="true" *ngFor="let ruleNodeType of ruleNodeTypesLibraryArray">
<mat-expansion-panel-header expandedHeight="48px"
(mouseenter)="typeHeaderMouseEnter($event, ruleNodeType)"
(mouseleave)="destroyTooltips()">
<mat-panel-title>
<mat-icon style="margin-right: 8px;">{{ ruleNodeTypeDescriptorsMap.get(ruleNodeType).icon }}</mat-icon>
<div class="tb-panel-title" translate>{{ ruleNodeTypeDescriptorsMap.get(ruleNodeType).name }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<fc-canvas id="tb-rulchain-{{ruleNodeType}}"
[model]="ruleNodeTypesModel[ruleNodeType].model"
[selectedObjects]="ruleNodeTypesModel[ruleNodeType].selectedObjects"
[automaticResize]="false"
[userCallbacks]="nodeLibCallbacks"
[nodeWidth]="170"
[nodeHeight]="50"
[dropTargetId]="'tb-rulchain-canvas'">
</fc-canvas>
</mat-expansion-panel>
</div>
</mat-drawer>
<mat-drawer-content>
<div class="tb-absolute-fill tb-rulechain-graph">
<fc-canvas #fcCanvas
<fc-canvas #ruleChainCanvas
id="tb-rulchain-canvas"
[model]="ruleChainModel"
(modelChanged)="onModelChanged()"
[selectedObjects]="selectedObjects"
[edgeStyle]="flowchartConstants.curvedStyle"
[automaticResize]="true"
[dragAnimation]="flowchartConstants.dragAnimationRepaint">
[nodeWidth]="170"
[nodeHeight]="50"
[dragAnimation]="flowchartConstants.dragAnimationRepaint"
[userCallbacks]="editCallbacks">
</fc-canvas>
</div>
</mat-drawer-content>

View File

@ -73,6 +73,32 @@
}
}
}
.tb-rulechain-library-panel-group {
overflow-x: hidden;
overflow-y: auto;
.mat-expansion-panel {
border-radius: 0px;
&:last-child {
margin-bottom: 5px;
}
.mat-expansion-panel-header {
background: #e6e6e6;
}
&.mat-expanded {
.mat-expansion-panel-header {
border-bottom: 1px solid;
border-color: #909090;
}
}
}
.tb-panel-title {
min-width: 140px;
user-select: none;
}
.fc-canvas {
background: #f9f9f9;
}
}
}
.tb-rulechain-graph {
z-index: 0;
@ -99,6 +125,11 @@
}
}
}
.tb-rulechain-library-panel-group {
.mat-expansion-panel-body {
padding: 0;
}
}
}
}
.fc-canvas {

View File

@ -14,14 +14,14 @@
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { AfterViewInit, Component, ElementRef, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder } from '@angular/forms';
import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material';
import { MatDialog, MatExpansionPanel } from '@angular/material';
import { DialogService } from '@core/services/dialog.service';
import { AuthService } from '@core/auth/auth.service';
import { ActivatedRoute, Router } from '@angular/router';
@ -31,26 +31,46 @@ import {
RuleChain,
ruleChainNodeComponent
} from '@shared/models/rule-chain.models';
import { FlowchartConstants } from 'ngx-flowchart/dist/ngx-flowchart';
import { RuleNodeComponentDescriptor, RuleNodeType, ruleNodeTypeDescriptors } from '@shared/models/rule-node.models';
import { FlowchartConstants, UserCallbacks, NgxFlowchartComponent } from 'ngx-flowchart/dist/ngx-flowchart';
import {
RuleNodeComponentDescriptor,
RuleNodeType,
ruleNodeTypeDescriptors,
ruleNodeTypesLibrary
} from '@shared/models/rule-node.models';
import { FcRuleEdge, FcRuleNode, FcRuleNodeModel, FcRuleNodeType, FcRuleNodeTypeModel } from './rulechain-page.models';
import { RuleChainService } from '@core/http/rule-chain.service';
import { fromEvent, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
import Timeout = NodeJS.Timeout;
import { ISearchableComponent } from '../../models/searchable-component.models';
@Component({
selector: 'tb-rulechain-page',
templateUrl: './rulechain-page.component.html',
styleUrls: ['./rulechain-page.component.scss']
})
export class RuleChainPageComponent extends PageComponent implements OnInit, HasDirtyFlag {
export class RuleChainPageComponent extends PageComponent
implements AfterViewInit, OnInit, HasDirtyFlag, ISearchableComponent {
get isDirty(): boolean {
return this.isDirtyValue || this.isImport;
}
@ViewChild('ruleNodeSearchInput', {static: false}) ruleNodeSearchInputField: ElementRef;
@ViewChild('ruleChainCanvas', {static: true}) ruleChainCanvas: NgxFlowchartComponent;
@ViewChildren('ruleNodeTypeExpansionPanels',
{read: MatExpansionPanel}) expansionPanels: QueryList<MatExpansionPanel>;
ruleNodeTypeDescriptorsMap = ruleNodeTypeDescriptors;
ruleNodeTypesLibraryArray = ruleNodeTypesLibrary;
isImport: boolean;
isDirtyValue: boolean;
errorTooltips = {};
errorTooltips: {[nodeId: string]: JQueryTooltipster.ITooltipsterInstance} = {};
isFullscreen = false;
editingRuleNode = null;
@ -62,6 +82,7 @@ export class RuleChainPageComponent extends PageComponent implements OnInit, Has
isLibraryOpen = true;
ruleNodeSearch = '';
ruleNodeTypeSearch = '';
ruleChain: RuleChain;
ruleChainMetaData: ResolvedRuleChainMetaData;
@ -71,16 +92,58 @@ export class RuleChainPageComponent extends PageComponent implements OnInit, Has
edges: []
};
selectedObjects = [];
editCallbacks: UserCallbacks = {
edgeDoubleClick: (event, edge) => {
console.log('TODO');
},
edgeEdit: (event, edge) => {
console.log('TODO');
},
nodeCallbacks: {
doubleClick: (event, node) => {
console.log('TODO');
},
nodeEdit: (event, node) => {
console.log('TODO');
},
mouseEnter: this.displayNodeDescriptionTooltip.bind(this),
mouseLeave: this.destroyTooltips.bind(this),
mouseDown: this.destroyTooltips.bind(this)
},
isValidEdge: (source, destination) => {
return source.type === FlowchartConstants.rightConnectorType && destination.type === FlowchartConstants.leftConnectorType;
},
createEdge: (event, edge) => {
console.log('TODO');
return of(edge);
},
dropNode: (event, node) => {
console.log('TODO dropNode');
console.log(node);
}
};
nextNodeID: number;
nextConnectorID: number;
inputConnectorId: number;
ruleNodeTypesModel: {[type: string]: {model: FcRuleNodeTypeModel, selectedObjects: any[]}} = {};
nodeLibCallbacks: UserCallbacks = {
nodeCallbacks: {
mouseEnter: this.displayLibNodeDescriptionTooltip.bind(this),
mouseLeave: this.destroyTooltips.bind(this),
mouseDown: this.destroyTooltips.bind(this)
}
};
ruleNodeComponents: Array<RuleNodeComponentDescriptor>;
flowchartConstants = FlowchartConstants;
private tooltipTimeout: Timeout;
constructor(protected store: Store<AppState>,
private route: ActivatedRoute,
private router: Router,
@ -97,6 +160,23 @@ export class RuleChainPageComponent extends PageComponent implements OnInit, Has
ngOnInit() {
}
ngAfterViewInit() {
fromEvent(this.ruleNodeSearchInputField.nativeElement, 'keyup')
.pipe(
debounceTime(150),
distinctUntilChanged(),
tap(() => {
this.updateRuleChainLibrary();
})
)
.subscribe();
}
onSearchTextUpdated(searchText: string) {
this.ruleNodeSearch = searchText;
this.updateRuleNodesHighlight();
}
private init() {
this.ruleChain = this.route.snapshot.data.ruleChain;
if (this.route.snapshot.data.import && !this.ruleChain) {
@ -106,8 +186,8 @@ export class RuleChainPageComponent extends PageComponent implements OnInit, Has
this.isImport = this.route.snapshot.data.import;
this.ruleChainMetaData = this.route.snapshot.data.ruleChainMetaData;
this.ruleNodeComponents = this.route.snapshot.data.ruleNodeComponents;
for (const type of Object.keys(RuleNodeType)) {
const desc = ruleNodeTypeDescriptors.get(RuleNodeType[type]);
for (const type of ruleNodeTypesLibrary) {
const desc = ruleNodeTypeDescriptors.get(type);
if (!desc.special) {
this.ruleNodeTypesModel[type] = {
model: {
@ -118,10 +198,17 @@ export class RuleChainPageComponent extends PageComponent implements OnInit, Has
};
}
}
this.loadRuleChainLibrary(this.ruleNodeComponents);
this.updateRuleChainLibrary();
this.createRuleChainModel();
}
private updateRuleChainLibrary() {
const search = this.ruleNodeTypeSearch.toUpperCase();
const res = this.ruleNodeComponents.filter(
(ruleNodeComponent) => ruleNodeComponent.name.toUpperCase().includes(search));
this.loadRuleChainLibrary(res);
}
private loadRuleChainLibrary(ruleNodeComponents: Array<RuleNodeComponentDescriptor>) {
for (const componentType of Object.keys(this.ruleNodeTypesModel)) {
this.ruleNodeTypesModel[componentType].model.nodes.length = 0;
@ -167,6 +254,21 @@ export class RuleChainPageComponent extends PageComponent implements OnInit, Has
}
model.nodes.push(node);
});
if (this.expansionPanels) {
for (let i = 0; i < ruleNodeTypesLibrary.length; i++) {
const panel = this.expansionPanels.find((item, index) => {
return index === i;
});
if (panel) {
const type = ruleNodeTypesLibrary[i];
if (!this.ruleNodeTypesModel[type].model.nodes.length) {
panel.close();
} else {
panel.open();
}
}
}
}
}
private createRuleChainModel() {
@ -350,5 +452,115 @@ export class RuleChainPageComponent extends PageComponent implements OnInit, Has
this.isDirtyValue = true;
}
typeHeaderMouseEnter(event: MouseEvent, ruleNodeType: RuleNodeType) {
const type = ruleNodeTypeDescriptors.get(ruleNodeType);
this.displayTooltip(event,
'<div class="tb-rule-node-tooltip tb-lib-tooltip">' +
'<div id="tb-node-content" layout="column">' +
'<div class="tb-node-title">' + this.translate.instant(type.name) + '</div>' +
'<div class="tb-node-details">' + this.translate.instant(type.details) + '</div>' +
'</div>' +
'</div>'
);
}
displayLibNodeDescriptionTooltip(event: MouseEvent, node: FcRuleNodeType) {
this.displayTooltip(event,
'<div class="tb-rule-node-tooltip tb-lib-tooltip">' +
'<div id="tb-node-content" layout="column">' +
'<div class="tb-node-title">' + node.component.name + '</div>' +
'<div class="tb-node-description">' + node.component.configurationDescriptor.nodeDefinition.description + '</div>' +
'<div class="tb-node-details">' + node.component.configurationDescriptor.nodeDefinition.details + '</div>' +
'</div>' +
'</div>'
);
}
displayNodeDescriptionTooltip(event: MouseEvent, node: FcRuleNode) {
if (!this.errorTooltips[node.id]) {
let name: string;
let desc: string;
let details: string;
if (node.component.type === RuleNodeType.INPUT) {
name = this.translate.instant(ruleNodeTypeDescriptors.get(RuleNodeType.INPUT).name);
desc = this.translate.instant(ruleNodeTypeDescriptors.get(RuleNodeType.INPUT).details);
} else {
name = node.name;
desc = this.translate.instant(ruleNodeTypeDescriptors.get(node.component.type).name) + ' - ' + node.component.name;
if (node.additionalInfo) {
details = node.additionalInfo.description;
}
}
let tooltipContent = '<div class="tb-rule-node-tooltip">' +
'<div id="tb-node-content" layout="column">' +
'<div class="tb-node-title">' + name + '</div>' +
'<div class="tb-node-description">' + desc + '</div>';
if (details) {
tooltipContent += '<div class="tb-node-details">' + details + '</div>';
}
tooltipContent += '</div>' +
'</div>';
this.displayTooltip(event, tooltipContent);
}
}
destroyTooltips() {
if (this.tooltipTimeout) {
clearTimeout(this.tooltipTimeout);
this.tooltipTimeout = null;
}
const instances = $.tooltipster.instances();
instances.forEach((instance) => {
if (!instance.isErrorTooltip) {
instance.destroy();
}
});
}
updateRuleNodesHighlight() {
for (const ruleNode of this.ruleChainModel.nodes) {
ruleNode.highlighted = false;
}
if (this.ruleNodeSearch) {
const search = this.ruleNodeSearch.toUpperCase();
const res = this.ruleChainModel.nodes.filter(node => node.name.toUpperCase().includes(search));
if (res) {
for (const ruleNode of res) {
ruleNode.highlighted = true;
}
}
}
this.ruleChainCanvas.modelService.detectChanges();
}
private displayTooltip(event: MouseEvent, content: string) {
this.destroyTooltips();
this.tooltipTimeout = setTimeout(() => {
const element = $(event.target);
element.tooltipster(
{
theme: 'tooltipster-shadow',
delay: 100,
trigger: 'custom',
triggerOpen: {
click: false,
tap: false
},
triggerClose: {
click: true,
tap: true,
scroll: true
},
side: 'right',
trackOrigin: true
}
);
const contentElement = $(content);
const tooltip = element.tooltipster('instance');
tooltip.content(contentElement);
tooltip.open();
}, 500);
}
}

View File

@ -37,6 +37,7 @@ export interface FcRuleNode extends FcRuleNodeType {
debugMode?: boolean;
targetRuleChainId?: string;
error?: string;
highlighted?: boolean;
}
export interface FcRuleEdge extends FcEdge {

View File

@ -0,0 +1,70 @@
/**
* 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.
*/
.tooltipster-base {
.tb-rule-node-tooltip,
.tb-rule-node-help {
color: #333;
}
.tb-rule-node-tooltip {
max-width: 300px;
font-size: 14px;
&.tb-lib-tooltip {
width: 300px;
}
}
.tb-rule-node-help {
font-size: 16px;
}
.tb-rule-node-error-tooltip {
font-size: 16px;
color: #ea0d0d;
}
.tb-rule-node-tooltip,
.tb-rule-node-error-tooltip,
.tb-rule-node-help {
#tb-node-content {
.tb-node-title {
font-weight: 600;
}
.tb-node-description {
font-style: italic;
color: #555;
}
.tb-node-details {
padding-top: 10px;
padding-bottom: 10px;
}
code {
padding: 0 3px 2px 3px;
margin: 1px;
font-size: 12px;
color: #ad1625;
white-space: nowrap;
background-color: #f7f7f9;
border: 1px solid #e1e1e8;
border-radius: 2px;
}
}
}
}

View File

@ -22,8 +22,8 @@
(mouseleave)="userNodeCallbacks.mouseLeave($event, node)">
<div class="{{flowchartConstants.nodeOverlayClass}}"></div>
<div class="tb-rule-node {{node.nodeClass}}" [ngClass]="{'tb-rule-node-highlighted' : node.highlighted, 'tb-rule-node-invalid': node.error }">
<mat-icon *ngIf="!node.iconUrl" fxFlex="15">{{node.icon}}</mat-icon>
<img *ngIf="node.iconUrl" fxFlex="15" src="{{node.iconUrl}}"/>
<mat-icon *ngIf="!iconUrl" fxFlex="15">{{node.icon}}</mat-icon>
<img *ngIf="iconUrl" fxFlex="15" [src]="iconUrl"/>
<div fxLayout="column" fxFlex="85" fxLayoutAlign="center">
<span class="tb-node-type">{{ node.component.name }}</span>
<span class="tb-node-title" *ngIf="node.name">{{ node.name }}</span>

View File

@ -14,7 +14,8 @@
/// limitations under the License.
///
import { Component } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { Component, OnInit } from '@angular/core';
import { FcNodeComponent } from 'ngx-flowchart/dist/ngx-flowchart';
@Component({
@ -22,10 +23,19 @@ import { FcNodeComponent } from 'ngx-flowchart/dist/ngx-flowchart';
templateUrl: './rulenode.component.html',
styleUrls: ['./rulenode.component.scss']
})
export class RuleNodeComponent extends FcNodeComponent {
export class RuleNodeComponent extends FcNodeComponent implements OnInit {
constructor() {
iconUrl: SafeResourceUrl;
constructor(private sanitizer: DomSanitizer) {
super();
}
ngOnInit(): void {
super.ngOnInit();
if (this.node.iconUrl) {
this.iconUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.node.iconUrl);
}
}
}

View File

@ -70,6 +70,15 @@ export enum RuleNodeType {
INPUT = 'INPUT'
}
export const ruleNodeTypesLibrary = [
RuleNodeType.FILTER,
RuleNodeType.ENRICHMENT,
RuleNodeType.TRANSFORMATION,
RuleNodeType.ACTION,
RuleNodeType.EXTERNAL,
RuleNodeType.RULE_CHAIN,
];
export interface RuleNodeTypeDescriptor {
value: RuleNodeType;
name: string;

View File

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