Commit 53b6aeb4fa1b9a976affa96d66563d7d6a939d12

Authored by Igor Kulikov
1 parent 08e5a7e0

Rule chain page. Inprove hotkeys handling

@@ -1407,14 +1407,12 @@ @@ -1407,14 +1407,12 @@
1407 "balanced-match": { 1407 "balanced-match": {
1408 "version": "1.0.0", 1408 "version": "1.0.0",
1409 "bundled": true, 1409 "bundled": true,
1410 - "dev": true,  
1411 - "optional": true 1410 + "dev": true
1412 }, 1411 },
1413 "brace-expansion": { 1412 "brace-expansion": {
1414 "version": "1.1.11", 1413 "version": "1.1.11",
1415 "bundled": true, 1414 "bundled": true,
1416 "dev": true, 1415 "dev": true,
1417 - "optional": true,  
1418 "requires": { 1416 "requires": {
1419 "balanced-match": "^1.0.0", 1417 "balanced-match": "^1.0.0",
1420 "concat-map": "0.0.1" 1418 "concat-map": "0.0.1"
@@ -1429,20 +1427,17 @@ @@ -1429,20 +1427,17 @@
1429 "code-point-at": { 1427 "code-point-at": {
1430 "version": "1.1.0", 1428 "version": "1.1.0",
1431 "bundled": true, 1429 "bundled": true,
1432 - "dev": true,  
1433 - "optional": true 1430 + "dev": true
1434 }, 1431 },
1435 "concat-map": { 1432 "concat-map": {
1436 "version": "0.0.1", 1433 "version": "0.0.1",
1437 "bundled": true, 1434 "bundled": true,
1438 - "dev": true,  
1439 - "optional": true 1435 + "dev": true
1440 }, 1436 },
1441 "console-control-strings": { 1437 "console-control-strings": {
1442 "version": "1.1.0", 1438 "version": "1.1.0",
1443 "bundled": true, 1439 "bundled": true,
1444 - "dev": true,  
1445 - "optional": true 1440 + "dev": true
1446 }, 1441 },
1447 "core-util-is": { 1442 "core-util-is": {
1448 "version": "1.0.2", 1443 "version": "1.0.2",
@@ -1559,8 +1554,7 @@ @@ -1559,8 +1554,7 @@
1559 "inherits": { 1554 "inherits": {
1560 "version": "2.0.3", 1555 "version": "2.0.3",
1561 "bundled": true, 1556 "bundled": true,
1562 - "dev": true,  
1563 - "optional": true 1557 + "dev": true
1564 }, 1558 },
1565 "ini": { 1559 "ini": {
1566 "version": "1.3.5", 1560 "version": "1.3.5",
@@ -1572,7 +1566,6 @@ @@ -1572,7 +1566,6 @@
1572 "version": "1.0.0", 1566 "version": "1.0.0",
1573 "bundled": true, 1567 "bundled": true,
1574 "dev": true, 1568 "dev": true,
1575 - "optional": true,  
1576 "requires": { 1569 "requires": {
1577 "number-is-nan": "^1.0.0" 1570 "number-is-nan": "^1.0.0"
1578 } 1571 }
@@ -1587,7 +1580,6 @@ @@ -1587,7 +1580,6 @@
1587 "version": "3.0.4", 1580 "version": "3.0.4",
1588 "bundled": true, 1581 "bundled": true,
1589 "dev": true, 1582 "dev": true,
1590 - "optional": true,  
1591 "requires": { 1583 "requires": {
1592 "brace-expansion": "^1.1.7" 1584 "brace-expansion": "^1.1.7"
1593 } 1585 }
@@ -1699,8 +1691,7 @@ @@ -1699,8 +1691,7 @@
1699 "number-is-nan": { 1691 "number-is-nan": {
1700 "version": "1.0.1", 1692 "version": "1.0.1",
1701 "bundled": true, 1693 "bundled": true,
1702 - "dev": true,  
1703 - "optional": true 1694 + "dev": true
1704 }, 1695 },
1705 "object-assign": { 1696 "object-assign": {
1706 "version": "4.1.1", 1697 "version": "4.1.1",
@@ -1712,7 +1703,6 @@ @@ -1712,7 +1703,6 @@
1712 "version": "1.4.0", 1703 "version": "1.4.0",
1713 "bundled": true, 1704 "bundled": true,
1714 "dev": true, 1705 "dev": true,
1715 - "optional": true,  
1716 "requires": { 1706 "requires": {
1717 "wrappy": "1" 1707 "wrappy": "1"
1718 } 1708 }
@@ -1834,7 +1824,6 @@ @@ -1834,7 +1824,6 @@
1834 "version": "1.0.2", 1824 "version": "1.0.2",
1835 "bundled": true, 1825 "bundled": true,
1836 "dev": true, 1826 "dev": true,
1837 - "optional": true,  
1838 "requires": { 1827 "requires": {
1839 "code-point-at": "^1.0.0", 1828 "code-point-at": "^1.0.0",
1840 "is-fullwidth-code-point": "^1.0.0", 1829 "is-fullwidth-code-point": "^1.0.0",
@@ -186,7 +186,9 @@ export class AliasController implements IAliasController { @@ -186,7 +186,9 @@ export class AliasController implements IAliasController {
186 ); 186 );
187 } else { 187 } else {
188 resolvedAliasSubject.error(null); 188 resolvedAliasSubject.error(null);
  189 + const res = this.resolvedAliasesObservable[aliasId];
189 delete this.resolvedAliasesObservable[aliasId]; 190 delete this.resolvedAliasesObservable[aliasId];
  191 + return res;
190 } 192 }
191 return this.resolvedAliasesObservable[aliasId]; 193 return this.resolvedAliasesObservable[aliasId];
192 } 194 }
@@ -24,6 +24,8 @@ import * as equal from 'deep-equal'; @@ -24,6 +24,8 @@ import * as equal from 'deep-equal';
24 import { UtilsService } from '@core/services/utils.service'; 24 import { UtilsService } from '@core/services/utils.service';
25 import { Observable, of, throwError } from 'rxjs'; 25 import { Observable, of, throwError } from 'rxjs';
26 import { map } from 'rxjs/operators'; 26 import { map } from 'rxjs/operators';
  27 +import { FcRuleEdge, FcRuleNode, ruleNodeTypeDescriptors } from '@shared/models/rule-node.models';
  28 +import { RuleChainService } from '@core/http/rule-chain.service';
27 29
28 const WIDGET_ITEM = 'widget_item'; 30 const WIDGET_ITEM = 'widget_item';
29 const WIDGET_REFERENCE = 'widget_reference'; 31 const WIDGET_REFERENCE = 'widget_reference';
@@ -45,6 +47,21 @@ export interface WidgetReference { @@ -45,6 +47,21 @@ export interface WidgetReference {
45 originalColumns: number; 47 originalColumns: number;
46 } 48 }
47 49
  50 +export interface RuleNodeConnection {
  51 + isInputSource: boolean;
  52 + fromIndex: number;
  53 + toIndex: number;
  54 + label: string;
  55 + labels: string[];
  56 +}
  57 +
  58 +export interface RuleNodesReference {
  59 + nodes: FcRuleNode[];
  60 + connections: RuleNodeConnection[];
  61 + originX?: number;
  62 + originY?: number;
  63 +}
  64 +
48 @Injectable({ 65 @Injectable({
49 providedIn: 'root' 66 providedIn: 'root'
50 }) 67 })
@@ -54,6 +71,7 @@ export class ItemBufferService { @@ -54,6 +71,7 @@ export class ItemBufferService {
54 private delimiter = '.'; 71 private delimiter = '.';
55 72
56 constructor(private dashboardUtils: DashboardUtilsService, 73 constructor(private dashboardUtils: DashboardUtilsService,
  74 + private ruleChainService: RuleChainService,
57 private utils: UtilsService) {} 75 private utils: UtilsService) {}
58 76
59 public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetItem { 77 public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetItem {
@@ -99,12 +117,12 @@ export class ItemBufferService { @@ -99,12 +117,12 @@ export class ItemBufferService {
99 117
100 public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { 118 public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void {
101 const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); 119 const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
102 - this.storeSet(WIDGET_ITEM, JSON.stringify(widgetItem)); 120 + this.storeSet(WIDGET_ITEM, widgetItem);
103 } 121 }
104 122
105 public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { 123 public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void {
106 const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget); 124 const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget);
107 - this.storeSet(WIDGET_REFERENCE, JSON.stringify(widgetReference)); 125 + this.storeSet(WIDGET_REFERENCE, widgetReference);
108 } 126 }
109 127
110 public hasWidget(): boolean { 128 public hasWidget(): boolean {
@@ -112,9 +130,8 @@ export class ItemBufferService { @@ -112,9 +130,8 @@ export class ItemBufferService {
112 } 130 }
113 131
114 public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId): boolean { 132 public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId): boolean {
115 - const widgetReferenceJson = this.storeGet(WIDGET_REFERENCE);  
116 - if (widgetReferenceJson) {  
117 - const widgetReference: WidgetReference = JSON.parse(widgetReferenceJson); 133 + const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE);
  134 + if (widgetReference) {
118 if (widgetReference.dashboardId === dashboard.id.id) { 135 if (widgetReference.dashboardId === dashboard.id.id) {
119 if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout) 136 if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout)
120 && dashboard.configuration.widgets[widgetReference.widgetId]) { 137 && dashboard.configuration.widgets[widgetReference.widgetId]) {
@@ -128,9 +145,8 @@ export class ItemBufferService { @@ -128,9 +145,8 @@ export class ItemBufferService {
128 public pasteWidget(targetDashboard: Dashboard, targetState: string, 145 public pasteWidget(targetDashboard: Dashboard, targetState: string,
129 targetLayout: DashboardLayoutId, position: WidgetPosition, 146 targetLayout: DashboardLayoutId, position: WidgetPosition,
130 onAliasesUpdateFunction: () => void): Observable<Widget> { 147 onAliasesUpdateFunction: () => void): Observable<Widget> {
131 - const widgetItemJson = this.storeGet(WIDGET_ITEM);  
132 - if (widgetItemJson) {  
133 - const widgetItem: WidgetItem = JSON.parse(widgetItemJson); 148 + const widgetItem: WidgetItem = this.storeGet(WIDGET_ITEM);
  149 + if (widgetItem) {
134 const widget = widgetItem.widget; 150 const widget = widgetItem.widget;
135 const aliasesInfo = widgetItem.aliasesInfo; 151 const aliasesInfo = widgetItem.aliasesInfo;
136 const originalColumns = widgetItem.originalColumns; 152 const originalColumns = widgetItem.originalColumns;
@@ -155,9 +171,8 @@ export class ItemBufferService { @@ -155,9 +171,8 @@ export class ItemBufferService {
155 171
156 public pasteWidgetReference(targetDashboard: Dashboard, targetState: string, 172 public pasteWidgetReference(targetDashboard: Dashboard, targetState: string,
157 targetLayout: DashboardLayoutId, position: WidgetPosition): Observable<Widget> { 173 targetLayout: DashboardLayoutId, position: WidgetPosition): Observable<Widget> {
158 - const widgetReferenceJson = this.storeGet(WIDGET_REFERENCE);  
159 - if (widgetReferenceJson) {  
160 - const widgetReference: WidgetReference = JSON.parse(widgetReferenceJson); 174 + const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE);
  175 + if (widgetReference) {
161 const widget = targetDashboard.configuration.widgets[widgetReference.widgetId]; 176 const widget = targetDashboard.configuration.widgets[widgetReference.widgetId];
162 if (widget) { 177 if (widget) {
163 const originalColumns = widgetReference.originalColumns; 178 const originalColumns = widgetReference.originalColumns;
@@ -216,6 +231,89 @@ export class ItemBufferService { @@ -216,6 +231,89 @@ export class ItemBufferService {
216 return of(theDashboard); 231 return of(theDashboard);
217 } 232 }
218 233
  234 + public copyRuleNodes(nodes: FcRuleNode[], connections: RuleNodeConnection[]) {
  235 + const ruleNodes: RuleNodesReference = {
  236 + nodes: [],
  237 + connections: []
  238 + };
  239 + let top = -1, left = -1, bottom = -1, right = -1;
  240 + for (let i = 0; i < nodes.length; i++) {
  241 + const origNode = nodes[i];
  242 + const node: FcRuleNode = {
  243 + id: '',
  244 + connectors: [],
  245 + additionalInfo: origNode.additionalInfo,
  246 + configuration: origNode.configuration,
  247 + debugMode: origNode.debugMode,
  248 + x: origNode.x,
  249 + y: origNode.y,
  250 + name: origNode.name,
  251 + componentClazz: origNode.component.clazz,
  252 + }
  253 + if (origNode.targetRuleChainId) {
  254 + node.targetRuleChainId = origNode.targetRuleChainId;
  255 + }
  256 + if (origNode.error) {
  257 + node.error = origNode.error;
  258 + }
  259 + ruleNodes.nodes.push(node);
  260 + if (i==0) {
  261 + top = node.y;
  262 + left = node.x;
  263 + bottom = node.y + 50;
  264 + right = node.x + 170;
  265 + } else {
  266 + top = Math.min(top, node.y);
  267 + left = Math.min(left, node.x);
  268 + bottom = Math.max(bottom, node.y + 50);
  269 + right = Math.max(right, node.x + 170);
  270 + }
  271 + }
  272 + ruleNodes.originX = left + (right-left)/2;
  273 + ruleNodes.originY = top + (bottom-top)/2;
  274 + connections.forEach(connection => {
  275 + ruleNodes.connections.push(connection);
  276 + });
  277 + this.storeSet(RULE_NODES, ruleNodes);
  278 + }
  279 +
  280 + public hasRuleNodes(): boolean {
  281 + return this.storeHas(RULE_NODES);
  282 + }
  283 +
  284 + public pasteRuleNodes(x: number, y: number): RuleNodesReference {
  285 + const ruleNodes: RuleNodesReference = this.storeGet(RULE_NODES);
  286 + if (ruleNodes) {
  287 + const deltaX = x - ruleNodes.originX;
  288 + const deltaY = y - ruleNodes.originY;
  289 + for (const node of ruleNodes.nodes) {
  290 + const component = this.ruleChainService.getRuleNodeComponentByClazz(node.componentClazz);
  291 + if (component) {
  292 + let icon = ruleNodeTypeDescriptors.get(component.type).icon;
  293 + let iconUrl: string = null;
  294 + if (component.configurationDescriptor.nodeDefinition.icon) {
  295 + icon = component.configurationDescriptor.nodeDefinition.icon;
  296 + }
  297 + if (component.configurationDescriptor.nodeDefinition.iconUrl) {
  298 + iconUrl = component.configurationDescriptor.nodeDefinition.iconUrl;
  299 + }
  300 + delete node.componentClazz;
  301 + node.component = component;
  302 + node.nodeClass = ruleNodeTypeDescriptors.get(component.type).nodeClass;
  303 + node.icon = icon;
  304 + node.iconUrl = iconUrl;
  305 + node.connectors = [];
  306 + node.x = Math.round(node.x + deltaX);
  307 + node.y = Math.round(node.y + deltaY);
  308 + } else {
  309 + return null;
  310 + }
  311 + }
  312 + return ruleNodes;
  313 + }
  314 + return null;
  315 + }
  316 +
219 private getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId): number { 317 private getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId): number {
220 let originalColumns = 24; 318 let originalColumns = 24;
221 let gridSettings = null; 319 let gridSettings = null;
@@ -737,18 +737,22 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -737,18 +737,22 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
737 dataLoading: (subscription) => { 737 dataLoading: (subscription) => {
738 if (this.loadingData !== subscription.loadingData) { 738 if (this.loadingData !== subscription.loadingData) {
739 this.loadingData = subscription.loadingData; 739 this.loadingData = subscription.loadingData;
740 - this.cd.detectChanges(); 740 + if (!this.destroyed) {
  741 + this.cd.detectChanges();
  742 + }
741 } 743 }
742 }, 744 },
743 legendDataUpdated: (subscription, detectChanges) => { 745 legendDataUpdated: (subscription, detectChanges) => {
744 - if (detectChanges) { 746 + if (detectChanges && !this.destroyed) {
745 this.cd.detectChanges(); 747 this.cd.detectChanges();
746 } 748 }
747 }, 749 },
748 timeWindowUpdated: (subscription, timeWindowConfig) => { 750 timeWindowUpdated: (subscription, timeWindowConfig) => {
749 this.ngZone.run(() => { 751 this.ngZone.run(() => {
750 this.widget.config.timewindow = timeWindowConfig; 752 this.widget.config.timewindow = timeWindowConfig;
751 - this.cd.detectChanges(); 753 + if (!this.destroyed) {
  754 + this.cd.detectChanges();
  755 + }
752 }); 756 });
753 } 757 }
754 }; 758 };
@@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
17 --> 17 -->
18 <div class="tb-dashboard-page mat-content" style="padding-top: 150px;" 18 <div class="tb-dashboard-page mat-content" style="padding-top: 150px;"
19 fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen"> 19 fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
  20 + <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
20 <section class="tb-dashboard-toolbar" 21 <section class="tb-dashboard-toolbar"
21 [ngClass]="{ 'tb-dashboard-toolbar-opened': toolbarOpened, 22 [ngClass]="{ 'tb-dashboard-toolbar-opened': toolbarOpened,
22 'tb-dashboard-toolbar-closed': !toolbarOpened }"> 23 'tb-dashboard-toolbar-closed': !toolbarOpened }">
@@ -146,6 +147,7 @@ @@ -146,6 +147,7 @@
146 [mode]="isMobile ? 'over' : 'side'" 147 [mode]="isMobile ? 'over' : 'side'"
147 [(opened)]="rightLayoutOpened"> 148 [(opened)]="rightLayoutOpened">
148 <tb-dashboard-layout style="height: 100%;" 149 <tb-dashboard-layout style="height: 100%;"
  150 + [dashboardCheatSheet]="cheatSheetComponent"
149 [layoutCtx]="layouts.right.layoutCtx" 151 [layoutCtx]="layouts.right.layoutCtx"
150 [dashboardCtx]="dashboardCtx" 152 [dashboardCtx]="dashboardCtx"
151 [isEdit]="isEdit" 153 [isEdit]="isEdit"
@@ -159,6 +161,7 @@ @@ -159,6 +161,7 @@
159 [ngStyle]="{width: mainLayoutWidth(), 161 [ngStyle]="{width: mainLayoutWidth(),
160 height: mainLayoutHeight()}"> 162 height: mainLayoutHeight()}">
161 <tb-dashboard-layout 163 <tb-dashboard-layout
  164 + [dashboardCheatSheet]="cheatSheetComponent"
162 [layoutCtx]="layouts.main.layoutCtx" 165 [layoutCtx]="layouts.main.layoutCtx"
163 [dashboardCtx]="dashboardCtx" 166 [dashboardCtx]="dashboardCtx"
164 [isEdit]="isEdit" 167 [isEdit]="isEdit"
@@ -15,13 +15,14 @@ @@ -15,13 +15,14 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<hotkeys-cheatsheet></hotkeys-cheatsheet>  
19 <div fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center" 18 <div fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center"
20 *ngIf="layoutCtx.widgets.isLoading()"> 19 *ngIf="layoutCtx.widgets.isLoading()">
21 <mat-spinner color="warn" mode="indeterminate" diameter="100"> 20 <mat-spinner color="warn" mode="indeterminate" diameter="100">
22 </mat-spinner> 21 </mat-spinner>
23 </div> 22 </div>
24 -<div class="mat-content" style="position: relative; width: 100%; height: 100%;" 23 +<div class="mat-content"
  24 + style="position: relative; width: 100%; height: 100%;" tb-hotkeys [hotkeys]="hotKeys"
  25 + [cheatSheet]="dashboardCheatSheet"
25 [style.backgroundImage]="backgroundImage" 26 [style.backgroundImage]="backgroundImage"
26 [ngStyle]="dashboardStyle"> 27 [ngStyle]="dashboardStyle">
27 <section *ngIf="layoutCtx.widgets.isEmpty()" fxLayoutAlign="center center" 28 <section *ngIf="layoutCtx.widgets.isEmpty()" fxLayoutAlign="center center"
@@ -14,27 +14,25 @@ @@ -14,27 +14,25 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, OnDestroy, OnInit, Input, ChangeDetectorRef, ViewChild } from '@angular/core';  
18 -import { StateControllerComponent } from '@home/pages/dashboard/states/state-controller.component'; 17 +import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
19 import { ILayoutController } from '@home/pages/dashboard/layout/layout.models'; 18 import { ILayoutController } from '@home/pages/dashboard/layout/layout.models';
20 import { DashboardContext, DashboardPageLayoutContext } from '@home/pages/dashboard/dashboard-page.models'; 19 import { DashboardContext, DashboardPageLayoutContext } from '@home/pages/dashboard/dashboard-page.models';
21 import { PageComponent } from '@shared/components/page.component'; 20 import { PageComponent } from '@shared/components/page.component';
22 import { Store } from '@ngrx/store'; 21 import { Store } from '@ngrx/store';
23 import { AppState } from '@core/core.state'; 22 import { AppState } from '@core/core.state';
24 import { Widget } from '@shared/models/widget.models'; 23 import { Widget } from '@shared/models/widget.models';
25 -import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';  
26 -import { GridsterComponent } from 'angular-gridster2';  
27 import { 24 import {
28 DashboardCallbacks, 25 DashboardCallbacks,
29 DashboardContextMenuItem, 26 DashboardContextMenuItem,
30 - IDashboardComponent, WidgetContextMenuItem 27 + IDashboardComponent,
  28 + WidgetContextMenuItem
31 } from '@home/models/dashboard-component.models'; 29 } from '@home/models/dashboard-component.models';
32 -import { Observable, of, Subscription } from 'rxjs'; 30 +import { Subscription } from 'rxjs';
33 import { Hotkey, HotkeysService } from 'angular2-hotkeys'; 31 import { Hotkey, HotkeysService } from 'angular2-hotkeys';
34 -import { getCurrentIsLoading } from '@core/interceptors/load.selectors';  
35 import { TranslateService } from '@ngx-translate/core'; 32 import { TranslateService } from '@ngx-translate/core';
36 import { ItemBufferService } from '@app/core/services/item-buffer.service'; 33 import { ItemBufferService } from '@app/core/services/item-buffer.service';
37 import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; 34 import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
  35 +import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component';
38 36
39 @Component({ 37 @Component({
40 selector: 'tb-dashboard-layout', 38 selector: 'tb-dashboard-layout',
@@ -47,6 +45,10 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo @@ -47,6 +45,10 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
47 dashboardStyle: {[klass: string]: any} = null; 45 dashboardStyle: {[klass: string]: any} = null;
48 backgroundImage: SafeStyle | string; 46 backgroundImage: SafeStyle | string;
49 47
  48 + hotKeys: Hotkey[] = [];
  49 +
  50 + @Input() dashboardCheatSheet: TbCheatSheetComponent;
  51 +
50 @Input() 52 @Input()
51 set layoutCtx(val: DashboardPageLayoutContext) { 53 set layoutCtx(val: DashboardPageLayoutContext) {
52 this.layoutCtxValue = val; 54 this.layoutCtxValue = val;
@@ -81,11 +83,11 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo @@ -81,11 +83,11 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
81 private rxSubscriptions = new Array<Subscription>(); 83 private rxSubscriptions = new Array<Subscription>();
82 84
83 constructor(protected store: Store<AppState>, 85 constructor(protected store: Store<AppState>,
84 - private hotkeysService: HotkeysService,  
85 private translate: TranslateService, 86 private translate: TranslateService,
86 private itembuffer: ItemBufferService, 87 private itembuffer: ItemBufferService,
87 private sanitizer: DomSanitizer) { 88 private sanitizer: DomSanitizer) {
88 super(store); 89 super(store);
  90 + this.initHotKeys();
89 } 91 }
90 92
91 ngOnInit(): void { 93 ngOnInit(): void {
@@ -95,7 +97,6 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo @@ -95,7 +97,6 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
95 this.dashboardCtx.runChangeDetection(); 97 this.dashboardCtx.runChangeDetection();
96 }) 98 })
97 ); 99 );
98 - this.initHotKeys();  
99 } 100 }
100 101
101 ngOnDestroy(): void { 102 ngOnDestroy(): void {
@@ -106,7 +107,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo @@ -106,7 +107,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
106 } 107 }
107 108
108 private initHotKeys(): void { 109 private initHotKeys(): void {
109 - this.hotkeysService.add( 110 + this.hotKeys.push(
110 new Hotkey('ctrl+c', (event: KeyboardEvent) => { 111 new Hotkey('ctrl+c', (event: KeyboardEvent) => {
111 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { 112 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
112 const widget = this.dashboard.getSelectedWidget(); 113 const widget = this.dashboard.getSelectedWidget();
@@ -119,7 +120,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo @@ -119,7 +120,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
119 }, null, 120 }, null,
120 this.translate.instant('action.copy')) 121 this.translate.instant('action.copy'))
121 ); 122 );
122 - this.hotkeysService.add( 123 + this.hotKeys.push(
123 new Hotkey('ctrl+r', (event: KeyboardEvent) => { 124 new Hotkey('ctrl+r', (event: KeyboardEvent) => {
124 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { 125 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
125 const widget = this.dashboard.getSelectedWidget(); 126 const widget = this.dashboard.getSelectedWidget();
@@ -132,7 +133,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo @@ -132,7 +133,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
132 }, null, 133 }, null,
133 this.translate.instant('action.copy-reference')) 134 this.translate.instant('action.copy-reference'))
134 ); 135 );
135 - this.hotkeysService.add( 136 + this.hotKeys.push(
136 new Hotkey('ctrl+v', (event: KeyboardEvent) => { 137 new Hotkey('ctrl+v', (event: KeyboardEvent) => {
137 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { 138 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
138 if (this.itembuffer.hasWidget()) { 139 if (this.itembuffer.hasWidget()) {
@@ -144,7 +145,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo @@ -144,7 +145,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
144 }, null, 145 }, null,
145 this.translate.instant('action.paste')) 146 this.translate.instant('action.paste'))
146 ); 147 );
147 - this.hotkeysService.add( 148 + this.hotKeys.push(
148 new Hotkey('ctrl+i', (event: KeyboardEvent) => { 149 new Hotkey('ctrl+i', (event: KeyboardEvent) => {
149 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { 150 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
150 if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(), 151 if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(),
@@ -157,7 +158,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo @@ -157,7 +158,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
157 }, null, 158 }, null,
158 this.translate.instant('action.paste-reference')) 159 this.translate.instant('action.paste-reference'))
159 ); 160 );
160 - this.hotkeysService.add( 161 + this.hotKeys.push(
161 new Hotkey('ctrl+x', (event: KeyboardEvent) => { 162 new Hotkey('ctrl+x', (event: KeyboardEvent) => {
162 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { 163 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
163 const widget = this.dashboard.getSelectedWidget(); 164 const widget = this.dashboard.getSelectedWidget();
@@ -14,31 +14,14 @@ @@ -14,31 +14,14 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import {  
18 - AfterViewInit,  
19 - Component, ElementRef,  
20 - EventEmitter, forwardRef,  
21 - Input,  
22 - OnChanges,  
23 - OnInit,  
24 - Output,  
25 - SimpleChanges,  
26 - ViewChild  
27 -} from '@angular/core';  
28 -import { PageComponent } from '@shared/components/page.component';  
29 -import { Store } from '@ngrx/store';  
30 -import { AppState } from '@core/core.state';  
31 -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, NgForm, Validators } from '@angular/forms';  
32 -import { FcRuleNode, FcRuleEdge } from './rulechain-page.models';  
33 -import { RuleNodeType, LinkLabel } from '@shared/models/rule-node.models';  
34 -import { EntityType } from '@shared/models/entity-type.models';  
35 -import { Observable, of, Subscription } from 'rxjs';  
36 -import { RuleChainService } from '@core/http/rule-chain.service'; 17 +import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
  18 +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
  19 +import { LinkLabel } from '@shared/models/rule-node.models';
  20 +import { Observable, of } from 'rxjs';
37 import { coerceBooleanProperty } from '@angular/cdk/coercion'; 21 import { coerceBooleanProperty } from '@angular/cdk/coercion';
38 import { deepClone } from '@core/utils'; 22 import { deepClone } from '@core/utils';
39 -import { EntityAlias } from '@shared/models/alias.models';  
40 import { TruncatePipe } from '@shared/pipe/truncate.pipe'; 23 import { TruncatePipe } from '@shared/pipe/truncate.pipe';
41 -import { MatChipList, MatAutocomplete, MatChipInputEvent, MatAutocompleteSelectedEvent } from '@angular/material'; 24 +import { MatAutocomplete, MatAutocompleteSelectedEvent, MatChipInputEvent, MatChipList } from '@angular/material';
42 import { TranslateService } from '@ngx-translate/core'; 25 import { TranslateService } from '@ngx-translate/core';
43 import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; 26 import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
44 import { map, mergeMap, share, startWith } from 'rxjs/operators'; 27 import { map, mergeMap, share, startWith } from 'rxjs/operators';
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +@mixin rule-node-colors {
  18 + &.tb-filter-type {
  19 + background-color: #f1e861;
  20 + }
  21 +
  22 + &.tb-enrichment-type {
  23 + background-color: #cdf14e;
  24 + }
  25 +
  26 + &.tb-transformation-type {
  27 + background-color: #79cef1;
  28 + }
  29 +
  30 + &.tb-action-type {
  31 + background-color: #f1928f;
  32 + }
  33 +
  34 + &.tb-external-type {
  35 + background-color: #fbc766;
  36 + }
  37 +
  38 + &.tb-rule-chain-type {
  39 + background-color: #d6c4f1;
  40 + }
  41 +
  42 + &.tb-unknown-type {
  43 + background-color: #f16c29;
  44 + }
  45 +
  46 +}
@@ -19,12 +19,10 @@ import { PageComponent } from '@shared/components/page.component'; @@ -19,12 +19,10 @@ import { PageComponent } from '@shared/components/page.component';
19 import { Store } from '@ngrx/store'; 19 import { Store } from '@ngrx/store';
20 import { AppState } from '@core/core.state'; 20 import { AppState } from '@core/core.state';
21 import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms'; 21 import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
22 -import { FcRuleNode } from './rulechain-page.models';  
23 -import { RuleNodeType } from '@shared/models/rule-node.models'; 22 +import { FcRuleNode, RuleNodeType } from '@shared/models/rule-node.models';
24 import { EntityType } from '@shared/models/entity-type.models'; 23 import { EntityType } from '@shared/models/entity-type.models';
25 import { Subscription } from 'rxjs'; 24 import { Subscription } from 'rxjs';
26 import { RuleChainService } from '@core/http/rule-chain.service'; 25 import { RuleChainService } from '@core/http/rule-chain.service';
27 -import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component';  
28 import { RuleNodeConfigComponent } from './rule-node-config.component'; 26 import { RuleNodeConfigComponent } from './rule-node-config.component';
29 27
30 @Component({ 28 @Component({
@@ -14,34 +14,12 @@ @@ -14,34 +14,12 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import {  
18 - AfterViewInit,  
19 - Component, ElementRef,  
20 - EventEmitter, forwardRef,  
21 - Input,  
22 - OnChanges,  
23 - OnInit,  
24 - Output,  
25 - SimpleChanges,  
26 - ViewChild  
27 -} from '@angular/core';  
28 -import { PageComponent } from '@shared/components/page.component';  
29 -import { Store } from '@ngrx/store';  
30 -import { AppState } from '@core/core.state'; 17 +import { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
31 import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, NgForm, Validators } from '@angular/forms'; 18 import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, NgForm, Validators } from '@angular/forms';
32 -import { FcRuleNode, FcRuleEdge } from './rulechain-page.models';  
33 -import { RuleNodeType, LinkLabel } from '@shared/models/rule-node.models';  
34 -import { EntityType } from '@shared/models/entity-type.models';  
35 -import { Observable, of, Subscription } from 'rxjs';  
36 -import { RuleChainService } from '@core/http/rule-chain.service'; 19 +import { FcRuleEdge, LinkLabel } from '@shared/models/rule-node.models';
37 import { coerceBooleanProperty } from '@angular/cdk/coercion'; 20 import { coerceBooleanProperty } from '@angular/cdk/coercion';
38 -import { deepClone } from '@core/utils';  
39 -import { EntityAlias } from '@shared/models/alias.models';  
40 import { TruncatePipe } from '@shared/pipe/truncate.pipe'; 21 import { TruncatePipe } from '@shared/pipe/truncate.pipe';
41 -import { MatChipList, MatAutocomplete, MatChipInputEvent, MatAutocompleteSelectedEvent } from '@angular/material';  
42 import { TranslateService } from '@ngx-translate/core'; 22 import { TranslateService } from '@ngx-translate/core';
43 -import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';  
44 -import { map, mergeMap, share } from 'rxjs/operators';  
45 23
46 @Component({ 24 @Component({
47 selector: 'tb-rule-node-link', 25 selector: 'tb-rule-node-link',
@@ -15,8 +15,10 @@ @@ -15,8 +15,10 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div class="mat-content" fxFlex tb-fullscreen [fullscreen]="isFullscreen" 18 +<div class="mat-content" fxFlex tb-fullscreen [fullscreen]="isFullscreen" tb-hotkeys [hotkeys]="hotKeys"
  19 + [cheatSheet]="cheatSheetComponent"
19 fxLayout="column" class="tb-rulechain"> 20 fxLayout="column" class="tb-rulechain">
  21 + <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
20 <section class="tb-rulechain-container" fxFlex fxLayout="column"> 22 <section class="tb-rulechain-container" fxFlex fxLayout="column">
21 <div class="tb-rulechain-layout" fxFlex fxLayout="row"> 23 <div class="tb-rulechain-layout" fxFlex fxLayout="row">
22 <section fxLayout="row" 24 <section fxLayout="row"
@@ -80,6 +82,7 @@ @@ -80,6 +82,7 @@
80 [model]="ruleNodeTypesModel[ruleNodeType].model" 82 [model]="ruleNodeTypesModel[ruleNodeType].model"
81 [selectedObjects]="ruleNodeTypesModel[ruleNodeType].selectedObjects" 83 [selectedObjects]="ruleNodeTypesModel[ruleNodeType].selectedObjects"
82 [automaticResize]="false" 84 [automaticResize]="false"
  85 + fitModelSizeByDefault
83 [userCallbacks]="nodeLibCallbacks" 86 [userCallbacks]="nodeLibCallbacks"
84 [nodeWidth]="170" 87 [nodeWidth]="170"
85 [nodeHeight]="50" 88 [nodeHeight]="50"
@@ -162,7 +165,38 @@ @@ -162,7 +165,38 @@
162 matTooltipPosition="above"> 165 matTooltipPosition="above">
163 <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> 166 <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
164 </button> 167 </button>
165 - <div class="tb-absolute-fill tb-rulechain-graph"> 168 + <div class="tb-absolute-fill tb-rulechain-graph" (contextmenu)="openRuleChainContextMenu($event)">
  169 + <div #ruleChainMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed"
  170 + [style.left]="ruleChainMenuPosition.x"
  171 + [style.top]="ruleChainMenuPosition.y"
  172 + [matMenuTriggerFor]="ruleChainMenu">
  173 + </div>
  174 + <mat-menu #ruleChainMenu="matMenu" class="tb-rule-chain-context-menu"
  175 + [overlapTrigger]="true">
  176 + <ng-template matMenuContent let-contextInfo="contextInfo">
  177 + <div class="tb-rule-chain-context-menu-container" (mouseleave)="onRuleChainContextMenuMouseLeave()">
  178 + <div class="tb-context-menu-header {{contextInfo.headerClass}}">
  179 + <mat-icon *ngIf="!contextInfo.iconUrl">{{contextInfo.icon}}</mat-icon>
  180 + <img *ngIf="contextInfo.iconUrl" [src]="contextInfo.iconUrl"/>
  181 + <div fxFlex>
  182 + <div class="tb-context-menu-title">{{contextInfo.title}}</div>
  183 + <div class="tb-context-menu-subtitle">{{contextInfo.subtitle}}</div>
  184 + </div>
  185 + </div>
  186 + <div *ngFor="let menuItem of contextInfo.menuItems">
  187 + <mat-divider *ngIf="menuItem.divider"></mat-divider>
  188 + <button *ngIf="!menuItem.divider"
  189 + mat-menu-item
  190 + [disabled]="!menuItem.enabled"
  191 + (click)="menuItem.action(contextMenuEvent)">
  192 + <span *ngIf="menuItem.shortcut" class="tb-alt-text"> {{ menuItem.shortcut | keyboardShortcut }}</span>
  193 + <mat-icon *ngIf="menuItem.icon">{{menuItem.icon}}</mat-icon>
  194 + <span translate>{{menuItem.value}}</span>
  195 + </button>
  196 + </div>
  197 + </div>
  198 + </ng-template>
  199 + </mat-menu>
166 <fc-canvas #ruleChainCanvas 200 <fc-canvas #ruleChainCanvas
167 id="tb-rulchain-canvas" 201 id="tb-rulchain-canvas"
168 [model]="ruleChainModel" 202 [model]="ruleChainModel"
@@ -170,6 +204,7 @@ @@ -170,6 +204,7 @@
170 [selectedObjects]="selectedObjects" 204 [selectedObjects]="selectedObjects"
171 [edgeStyle]="flowchartConstants.curvedStyle" 205 [edgeStyle]="flowchartConstants.curvedStyle"
172 [automaticResize]="true" 206 [automaticResize]="true"
  207 + fitModelSizeByDefault="false"
173 [nodeWidth]="170" 208 [nodeWidth]="170"
174 [nodeHeight]="50" 209 [nodeHeight]="50"
175 [dragAnimation]="flowchartConstants.dragAnimationRepaint" 210 [dragAnimation]="flowchartConstants.dragAnimationRepaint"
@@ -14,6 +14,8 @@ @@ -14,6 +14,8 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 16
  17 +@import './rule-node-colors';
  18 +
17 .tb-rulechain { 19 .tb-rulechain {
18 width: 100%; 20 width: 100%;
19 height: 100%; 21 height: 100%;
@@ -267,3 +269,60 @@ @@ -267,3 +269,60 @@
267 } 269 }
268 } 270 }
269 } 271 }
  272 +
  273 +.tb-rule-chain-context-menu {
  274 + min-width: 256px;
  275 + max-height: 404px;
  276 + border-radius: 8px;
  277 + margin-left: -20px;
  278 +
  279 + &.mat-menu-below {
  280 + margin-top: -60px;
  281 + }
  282 +
  283 + .mat-menu-content {
  284 + padding: 0;
  285 + display: flex;
  286 + flex-direction: column;
  287 + .tb-rule-chain-context-menu-container {
  288 + pointer-events: auto;
  289 + padding: 0 0 8px;
  290 + display: flex;
  291 + flex-direction: column;
  292 + overflow-y: auto;
  293 + }
  294 + }
  295 +
  296 + .tb-context-menu-header {
  297 + display: flex;
  298 + flex-direction: row;
  299 + height: 36px;
  300 + min-height: 36px;
  301 + padding: 8px 5px 5px;
  302 + font-size: 14px;
  303 +
  304 + @include rule-node-colors();
  305 +
  306 + &.tb-rulechain-header {
  307 + background-color: #aac7e4;
  308 + }
  309 +
  310 + &.tb-link-header {
  311 + background-color: #aac7e4;
  312 + }
  313 +
  314 + .mat-icon {
  315 + padding-right: 10px;
  316 + padding-left: 2px;
  317 + margin: auto;
  318 + }
  319 +
  320 + .tb-context-menu-title {
  321 + font-weight: 500;
  322 + }
  323 +
  324 + .tb-context-menu-subtitle {
  325 + font-size: 12px;
  326 + }
  327 + }
  328 +}
@@ -18,9 +18,11 @@ import { @@ -18,9 +18,11 @@ import {
18 AfterViewInit, 18 AfterViewInit,
19 Component, 19 Component,
20 ElementRef, 20 ElementRef,
21 - HostBinding, Inject, 21 + HostBinding,
  22 + Inject,
22 OnInit, 23 OnInit,
23 - QueryList, SkipSelf, 24 + QueryList,
  25 + SkipSelf,
24 ViewChild, 26 ViewChild,
25 ViewChildren, 27 ViewChildren,
26 ViewEncapsulation 28 ViewEncapsulation
@@ -31,7 +33,7 @@ import { AppState } from '@core/core.state'; @@ -31,7 +33,7 @@ import { AppState } from '@core/core.state';
31 import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; 33 import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
32 import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; 34 import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
33 import { TranslateService } from '@ngx-translate/core'; 35 import { TranslateService } from '@ngx-translate/core';
34 -import { MatDialog, MatExpansionPanel, ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; 36 +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialog, MatDialogRef, MatExpansionPanel } from '@angular/material';
35 import { DialogService } from '@core/services/dialog.service'; 37 import { DialogService } from '@core/services/dialog.service';
36 import { AuthService } from '@core/auth/auth.service'; 38 import { AuthService } from '@core/auth/auth.service';
37 import { ActivatedRoute, Router } from '@angular/router'; 39 import { ActivatedRoute, Router } from '@angular/router';
@@ -41,8 +43,11 @@ import { @@ -41,8 +43,11 @@ import {
41 RuleChain, 43 RuleChain,
42 ruleChainNodeComponent 44 ruleChainNodeComponent
43 } from '@shared/models/rule-chain.models'; 45 } from '@shared/models/rule-chain.models';
44 -import { FlowchartConstants, NgxFlowchartComponent, UserCallbacks } from 'ngx-flowchart/dist/ngx-flowchart'; 46 +import { FcItemInfo, FlowchartConstants, NgxFlowchartComponent, UserCallbacks } from 'ngx-flowchart/dist/ngx-flowchart';
45 import { 47 import {
  48 + FcRuleEdge,
  49 + FcRuleNode,
  50 + FcRuleNodeType,
46 getRuleNodeHelpLink, 51 getRuleNodeHelpLink,
47 LinkLabel, 52 LinkLabel,
48 RuleNodeComponentDescriptor, 53 RuleNodeComponentDescriptor,
@@ -50,24 +55,19 @@ import { @@ -50,24 +55,19 @@ import {
50 ruleNodeTypeDescriptors, 55 ruleNodeTypeDescriptors,
51 ruleNodeTypesLibrary 56 ruleNodeTypesLibrary
52 } from '@shared/models/rule-node.models'; 57 } from '@shared/models/rule-node.models';
53 -import { FcRuleEdge, FcRuleNode, FcRuleNodeModel, FcRuleNodeType, FcRuleNodeTypeModel } from './rulechain-page.models'; 58 +import { FcRuleNodeModel, FcRuleNodeTypeModel, RuleChainMenuContextInfo } from './rulechain-page.models';
54 import { RuleChainService } from '@core/http/rule-chain.service'; 59 import { RuleChainService } from '@core/http/rule-chain.service';
55 -import { fromEvent, never, of, throwError, NEVER, Observable } from 'rxjs';  
56 -import { debounceTime, distinctUntilChanged, map, tap, mergeMap } from 'rxjs/operators'; 60 +import { fromEvent, NEVER, Observable, of } from 'rxjs';
  61 +import { debounceTime, distinctUntilChanged, mergeMap, tap } from 'rxjs/operators';
57 import { ISearchableComponent } from '../../models/searchable-component.models'; 62 import { ISearchableComponent } from '../../models/searchable-component.models';
58 -import { deepClone, isDefined, isString } from '@core/utils'; 63 +import { deepClone } from '@core/utils';
59 import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-details.component'; 64 import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-details.component';
60 import { RuleNodeLinkComponent } from './rule-node-link.component'; 65 import { RuleNodeLinkComponent } from './rule-node-link.component';
61 -import Timeout = NodeJS.Timeout;  
62 -import { Dashboard } from '@shared/models/dashboard.models';  
63 -import { IAliasController } from '@core/api/widget-api.models';  
64 -import { Widget, widgetTypesData } from '@shared/models/widget.models';  
65 -import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models';  
66 import { DialogComponent } from '@shared/components/dialog.component'; 66 import { DialogComponent } from '@shared/components/dialog.component';
67 -import { UtilsService } from '@core/services/utils.service';  
68 -import { EntityService } from '@core/http/entity.service';  
69 -import { AddWidgetDialogComponent, AddWidgetDialogData } from '@home/pages/dashboard/add-widget-dialog.component';  
70 -import { RuleNodeConfigComponent } from '@home/pages/rulechain/rule-node-config.component'; 67 +import { MatMenuTrigger } from '@angular/material/menu';
  68 +import { ItemBufferService, RuleNodeConnection } from '@core/services/item-buffer.service';
  69 +import Timeout = NodeJS.Timeout;
  70 +import { Hotkey, HotkeysService } from 'angular2-hotkeys';
71 71
72 @Component({ 72 @Component({
73 selector: 'tb-rulechain-page', 73 selector: 'tb-rulechain-page',
@@ -92,6 +92,12 @@ export class RuleChainPageComponent extends PageComponent @@ -92,6 +92,12 @@ export class RuleChainPageComponent extends PageComponent
92 @ViewChildren('ruleNodeTypeExpansionPanels', 92 @ViewChildren('ruleNodeTypeExpansionPanels',
93 {read: MatExpansionPanel}) expansionPanels: QueryList<MatExpansionPanel>; 93 {read: MatExpansionPanel}) expansionPanels: QueryList<MatExpansionPanel>;
94 94
  95 + @ViewChild('ruleChainMenuTrigger', {static: true}) ruleChainMenuTrigger: MatMenuTrigger;
  96 +
  97 + ruleChainMenuPosition = { x: '0px', y: '0px' };
  98 +
  99 + contextMenuEvent: MouseEvent;
  100 +
95 ruleNodeTypeDescriptorsMap = ruleNodeTypeDescriptors; 101 ruleNodeTypeDescriptorsMap = ruleNodeTypeDescriptors;
96 ruleNodeTypesLibraryArray = ruleNodeTypesLibrary; 102 ruleNodeTypesLibraryArray = ruleNodeTypesLibrary;
97 103
@@ -116,6 +122,9 @@ export class RuleChainPageComponent extends PageComponent @@ -116,6 +122,9 @@ export class RuleChainPageComponent extends PageComponent
116 isEditingRuleNodeLink = false; 122 isEditingRuleNodeLink = false;
117 editingRuleNodeLinkIndex = -1; 123 editingRuleNodeLinkIndex = -1;
118 124
  125 + hotKeys: Hotkey[] = [];
  126 +
  127 + enableHotKeys = true;
119 isLibraryOpen = true; 128 isLibraryOpen = true;
120 129
121 ruleNodeSearch = ''; 130 ruleNodeSearch = '';
@@ -173,7 +182,11 @@ export class RuleChainPageComponent extends PageComponent @@ -173,7 +182,11 @@ export class RuleChainPageComponent extends PageComponent
173 } else { 182 } else {
174 const labels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); 183 const labels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
175 const allowCustomLabels = this.ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component); 184 const allowCustomLabels = this.ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component);
  185 + this.enableHotKeys = false;
176 return this.addRuleNodeLink(edge, labels, allowCustomLabels).pipe( 186 return this.addRuleNodeLink(edge, labels, allowCustomLabels).pipe(
  187 + tap(() => {
  188 + this.enableHotKeys = true;
  189 + }),
177 mergeMap((res) => { 190 mergeMap((res) => {
178 if (res) { 191 if (res) {
179 return of(res); 192 return of(res);
@@ -216,6 +229,7 @@ export class RuleChainPageComponent extends PageComponent @@ -216,6 +229,7 @@ export class RuleChainPageComponent extends PageComponent
216 private ruleChainService: RuleChainService, 229 private ruleChainService: RuleChainService,
217 private authService: AuthService, 230 private authService: AuthService,
218 private translate: TranslateService, 231 private translate: TranslateService,
  232 + private itembuffer: ItemBufferService,
219 public dialog: MatDialog, 233 public dialog: MatDialog,
220 public dialogService: DialogService, 234 public dialogService: DialogService,
221 public fb: FormBuilder) { 235 public fb: FormBuilder) {
@@ -236,6 +250,7 @@ export class RuleChainPageComponent extends PageComponent @@ -236,6 +250,7 @@ export class RuleChainPageComponent extends PageComponent
236 }) 250 })
237 ) 251 )
238 .subscribe(); 252 .subscribe();
  253 + this.ruleChainCanvas.adjustCanvasSize(true);
239 } 254 }
240 255
241 onSearchTextUpdated(searchText: string) { 256 onSearchTextUpdated(searchText: string) {
@@ -244,6 +259,7 @@ export class RuleChainPageComponent extends PageComponent @@ -244,6 +259,7 @@ export class RuleChainPageComponent extends PageComponent
244 } 259 }
245 260
246 private init() { 261 private init() {
  262 + this.initHotKeys();
247 this.ruleChain = this.route.snapshot.data.ruleChain; 263 this.ruleChain = this.route.snapshot.data.ruleChain;
248 if (this.route.snapshot.data.import && !this.ruleChain) { 264 if (this.route.snapshot.data.import && !this.ruleChain) {
249 this.router.navigateByUrl('ruleChains'); 265 this.router.navigateByUrl('ruleChains');
@@ -268,6 +284,89 @@ export class RuleChainPageComponent extends PageComponent @@ -268,6 +284,89 @@ export class RuleChainPageComponent extends PageComponent
268 this.createRuleChainModel(); 284 this.createRuleChainModel();
269 } 285 }
270 286
  287 + private initHotKeys(): void {
  288 + this.hotKeys.push(
  289 + new Hotkey('ctrl+a', (event: KeyboardEvent) => {
  290 + if (this.enableHotKeys) {
  291 + event.preventDefault();
  292 + this.ruleChainCanvas.modelService.selectAll();
  293 + return false;
  294 + }
  295 + return true;
  296 + }, ['INPUT', 'SELECT', 'TEXTAREA'],
  297 + this.translate.instant('rulenode.select-all-objects'))
  298 + );
  299 + this.hotKeys.push(
  300 + new Hotkey('ctrl+c', (event: KeyboardEvent) => {
  301 + if (this.enableHotKeys) {
  302 + event.preventDefault();
  303 + this.copyRuleNodes();
  304 + return false;
  305 + }
  306 + return true;
  307 + }, ['INPUT', 'SELECT', 'TEXTAREA'],
  308 + this.translate.instant('rulenode.copy-selected'))
  309 + );
  310 + this.hotKeys.push(
  311 + new Hotkey('ctrl+v', (event: KeyboardEvent) => {
  312 + if (this.enableHotKeys) {
  313 + event.preventDefault();
  314 + if (this.itembuffer.hasRuleNodes()) {
  315 + this.pasteRuleNodes();
  316 + }
  317 + return false;
  318 + }
  319 + return true;
  320 + }, ['INPUT', 'SELECT', 'TEXTAREA'],
  321 + this.translate.instant('action.paste'))
  322 + );
  323 + this.hotKeys.push(
  324 + new Hotkey('esc', (event: KeyboardEvent) => {
  325 + if (this.enableHotKeys) {
  326 + event.preventDefault();
  327 + event.stopPropagation();
  328 + this.ruleChainCanvas.modelService.deselectAll();
  329 + return false;
  330 + }
  331 + return true;
  332 + }, ['INPUT', 'SELECT', 'TEXTAREA'],
  333 + this.translate.instant('rulenode.deselect-all-objects'))
  334 + );
  335 + this.hotKeys.push(
  336 + new Hotkey('ctrl+s', (event: KeyboardEvent) => {
  337 + if (this.enableHotKeys) {
  338 + event.preventDefault();
  339 + this.saveRuleChain();
  340 + return false;
  341 + }
  342 + return true;
  343 + }, ['INPUT', 'SELECT', 'TEXTAREA'],
  344 + this.translate.instant('action.apply'))
  345 + );
  346 + this.hotKeys.push(
  347 + new Hotkey('ctrl+z', (event: KeyboardEvent) => {
  348 + if (this.enableHotKeys) {
  349 + event.preventDefault();
  350 + this.revertRuleChain();
  351 + return false;
  352 + }
  353 + return true;
  354 + }, ['INPUT', 'SELECT', 'TEXTAREA'],
  355 + this.translate.instant('action.decline-changes'))
  356 + );
  357 + this.hotKeys.push(
  358 + new Hotkey('del', (event: KeyboardEvent) => {
  359 + if (this.enableHotKeys) {
  360 + event.preventDefault();
  361 + this.ruleChainCanvas.modelService.deleteSelected();
  362 + return false;
  363 + }
  364 + return true;
  365 + }, ['INPUT', 'SELECT', 'TEXTAREA'],
  366 + this.translate.instant('rulenode.delete-selected-objects'))
  367 + );
  368 + }
  369 +
271 updateRuleChainLibrary() { 370 updateRuleChainLibrary() {
272 const search = this.ruleNodeTypeSearch.toUpperCase(); 371 const search = this.ruleNodeTypeSearch.toUpperCase();
273 const res = this.ruleNodeComponents.filter( 372 const res = this.ruleNodeComponents.filter(
@@ -510,11 +609,229 @@ export class RuleChainPageComponent extends PageComponent @@ -510,11 +609,229 @@ export class RuleChainPageComponent extends PageComponent
510 } 609 }
511 }); 610 });
512 } 611 }
  612 + if (this.ruleChainCanvas) {
  613 + this.ruleChainCanvas.adjustCanvasSize(true);
  614 + }
513 this.isDirtyValue = false; 615 this.isDirtyValue = false;
514 this.updateRuleNodesHighlight(); 616 this.updateRuleNodesHighlight();
515 this.validate(); 617 this.validate();
516 } 618 }
517 619
  620 + openRuleChainContextMenu($event: MouseEvent) {
  621 + if (this.ruleChainCanvas.modelService && !$event.ctrlKey && !$event.metaKey) {
  622 + const x = $event.clientX;
  623 + const y = $event.clientY;
  624 + const item = this.ruleChainCanvas.modelService.getItemInfoAtPoint(x, y);
  625 + const contextInfo = this.prepareContextMenu(item);
  626 + if (contextInfo.menuItems && contextInfo.menuItems.length > 0) {
  627 + $event.preventDefault();
  628 + $event.stopPropagation();
  629 + this.contextMenuEvent = $event;
  630 + this.ruleChainMenuPosition.x = x + 'px';
  631 + this.ruleChainMenuPosition.y = y + 'px';
  632 + this.ruleChainMenuTrigger.menuData = { contextInfo };
  633 + this.ruleChainMenuTrigger.openMenu();
  634 + }
  635 + }
  636 + }
  637 +
  638 + onRuleChainContextMenuMouseLeave() {
  639 + this.ruleChainMenuTrigger.closeMenu();
  640 + }
  641 +
  642 + private prepareContextMenu(item: FcItemInfo): RuleChainMenuContextInfo {
  643 + if (this.objectsSelected() || (!item.node && !item.edge)) {
  644 + return this.prepareRuleChainContextMenu();
  645 + } else if (item.node) {
  646 + return this.prepareRuleNodeContextMenu(item.node);
  647 + } else if (item.edge) {
  648 + return this.prepareEdgeContextMenu(item.edge);
  649 + }
  650 + }
  651 +
  652 + private prepareRuleChainContextMenu(): RuleChainMenuContextInfo {
  653 + const contextInfo: RuleChainMenuContextInfo = {
  654 + headerClass: 'tb-rulechain-header',
  655 + icon: 'settings_ethernet',
  656 + title: this.ruleChain.name,
  657 + subtitle: this.translate.instant('rulechain.rulechain'),
  658 + menuItems: []
  659 + };
  660 + if (this.ruleChainCanvas.modelService.nodes.getSelectedNodes().length) {
  661 + contextInfo.menuItems.push(
  662 + {
  663 + action: () => {
  664 + this.copyRuleNodes();
  665 + },
  666 + enabled: true,
  667 + value: 'rulenode.copy-selected',
  668 + icon: 'content_copy',
  669 + shortcut: 'M-C'
  670 + }
  671 + );
  672 + }
  673 + contextInfo.menuItems.push(
  674 + {
  675 + action: ($event) => {
  676 + this.pasteRuleNodes($event);
  677 + },
  678 + enabled: this.itembuffer.hasRuleNodes(),
  679 + value: 'action.paste',
  680 + icon: 'content_paste',
  681 + shortcut: 'M-V'
  682 + }
  683 + );
  684 + contextInfo.menuItems.push(
  685 + {
  686 + divider: true
  687 + }
  688 + );
  689 + if (this.objectsSelected()) {
  690 + contextInfo.menuItems.push(
  691 + {
  692 + action: () => {
  693 + this.ruleChainCanvas.modelService.deselectAll();
  694 + },
  695 + enabled: true,
  696 + value: 'rulenode.deselect-all',
  697 + icon: 'tab_unselected',
  698 + shortcut: 'Esc'
  699 + }
  700 + );
  701 + contextInfo.menuItems.push(
  702 + {
  703 + action: () => {
  704 + this.ruleChainCanvas.modelService.deleteSelected();
  705 + },
  706 + enabled: true,
  707 + value: 'rulenode.delete-selected',
  708 + icon: 'clear',
  709 + shortcut: 'Del'
  710 + }
  711 + );
  712 + } else {
  713 + contextInfo.menuItems.push(
  714 + {
  715 + action: () => {
  716 + this.ruleChainCanvas.modelService.selectAll();
  717 + },
  718 + enabled: true,
  719 + value: 'rulenode.select-all',
  720 + icon: 'select_all',
  721 + shortcut: 'M-A'
  722 + }
  723 + );
  724 + }
  725 + contextInfo.menuItems.push(
  726 + {
  727 + divider: true
  728 + }
  729 + );
  730 + contextInfo.menuItems.push(
  731 + {
  732 + action: () => {
  733 + this.saveRuleChain();
  734 + },
  735 + enabled: !(this.isInvalid || (!this.isDirty && !this.isImport)),
  736 + value: 'action.apply-changes',
  737 + icon: 'done',
  738 + shortcut: 'M-S'
  739 + }
  740 + );
  741 + contextInfo.menuItems.push(
  742 + {
  743 + action: () => {
  744 + this.revertRuleChain();
  745 + },
  746 + enabled: this.isDirty,
  747 + value: 'action.decline-changes',
  748 + icon: 'close',
  749 + shortcut: 'M-Z'
  750 + }
  751 + );
  752 + return contextInfo;
  753 + }
  754 +
  755 + private prepareRuleNodeContextMenu(node: FcRuleNode): RuleChainMenuContextInfo {
  756 + const contextInfo: RuleChainMenuContextInfo = {
  757 + headerClass: node.nodeClass,
  758 + icon: node.icon,
  759 + iconUrl: node.iconUrl,
  760 + title: node.name,
  761 + subtitle: node.component.name,
  762 + menuItems: []
  763 + };
  764 + if (!node.readonly) {
  765 + contextInfo.menuItems.push(
  766 + {
  767 + action: () => {
  768 + this.openNodeDetails(node);
  769 + },
  770 + enabled: true,
  771 + value: 'rulenode.details',
  772 + icon: 'menu'
  773 + }
  774 + );
  775 + contextInfo.menuItems.push(
  776 + {
  777 + action: () => {
  778 + this.copyNode(node);
  779 + },
  780 + enabled: true,
  781 + value: 'action.copy',
  782 + icon: 'content_copy'
  783 + }
  784 + );
  785 + contextInfo.menuItems.push(
  786 + {
  787 + action: () => {
  788 + this.ruleChainCanvas.modelService.nodes.delete(node);
  789 + },
  790 + enabled: true,
  791 + value: 'action.delete',
  792 + icon: 'clear',
  793 + shortcut: 'M-X'
  794 + }
  795 + );
  796 + }
  797 + return contextInfo;
  798 + }
  799 +
  800 + private prepareEdgeContextMenu(edge: FcRuleEdge): RuleChainMenuContextInfo {
  801 + const contextInfo: RuleChainMenuContextInfo = {
  802 + headerClass: 'tb-link-header',
  803 + icon: 'trending_flat',
  804 + title: edge.label,
  805 + subtitle: this.translate.instant('rulenode.link'),
  806 + menuItems: []
  807 + };
  808 + const sourceNode: FcRuleNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source);
  809 + if (sourceNode.component.type != RuleNodeType.INPUT) {
  810 + contextInfo.menuItems.push(
  811 + {
  812 + action: () => {
  813 + this.openLinkDetails(edge);
  814 + },
  815 + enabled: true,
  816 + value: 'rulenode.details',
  817 + icon: 'menu'
  818 + }
  819 + );
  820 + }
  821 + contextInfo.menuItems.push(
  822 + {
  823 + action: () => {
  824 + this.ruleChainCanvas.modelService.edges.delete(edge);
  825 + },
  826 + enabled: true,
  827 + value: 'action.delete',
  828 + icon: 'clear',
  829 + shortcut: 'M-X'
  830 + }
  831 + );
  832 + return contextInfo;
  833 + }
  834 +
518 onModelChanged() { 835 onModelChanged() {
519 console.log('Model changed!'); 836 console.log('Model changed!');
520 this.isDirtyValue = true; 837 this.isDirtyValue = true;
@@ -531,6 +848,8 @@ export class RuleChainPageComponent extends PageComponent @@ -531,6 +848,8 @@ export class RuleChainPageComponent extends PageComponent
531 848
532 openNodeDetails(node: FcRuleNode) { 849 openNodeDetails(node: FcRuleNode) {
533 if (node.component.type !== RuleNodeType.INPUT) { 850 if (node.component.type !== RuleNodeType.INPUT) {
  851 + this.enableHotKeys = false;
  852 + this.updateErrorTooltips(true);
534 this.isEditingRuleNodeLink = false; 853 this.isEditingRuleNodeLink = false;
535 this.editingRuleNodeLink = null; 854 this.editingRuleNodeLink = null;
536 this.isEditingRuleNode = true; 855 this.isEditingRuleNode = true;
@@ -545,6 +864,8 @@ export class RuleChainPageComponent extends PageComponent @@ -545,6 +864,8 @@ export class RuleChainPageComponent extends PageComponent
545 openLinkDetails(edge: FcRuleEdge) { 864 openLinkDetails(edge: FcRuleEdge) {
546 const sourceNode: FcRuleNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source) as FcRuleNode; 865 const sourceNode: FcRuleNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source) as FcRuleNode;
547 if (sourceNode.component.type !== RuleNodeType.INPUT) { 866 if (sourceNode.component.type !== RuleNodeType.INPUT) {
  867 + this.enableHotKeys = false;
  868 + this.updateErrorTooltips(true);
548 this.isEditingRuleNode = false; 869 this.isEditingRuleNode = false;
549 this.editingRuleNode = null; 870 this.editingRuleNode = null;
550 this.editingRuleNodeLinkLabels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); 871 this.editingRuleNodeLinkLabels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
@@ -558,9 +879,121 @@ export class RuleChainPageComponent extends PageComponent @@ -558,9 +879,121 @@ export class RuleChainPageComponent extends PageComponent
558 } 879 }
559 } 880 }
560 881
  882 + private copyNode(node: FcRuleNode) {
  883 + this.itembuffer.copyRuleNodes([node], []);
  884 + }
  885 +
  886 + private copyRuleNodes() {
  887 + const nodes: FcRuleNode[] = this.ruleChainCanvas.modelService.nodes.getSelectedNodes();
  888 + const edges: FcRuleEdge[] = this.ruleChainCanvas.modelService.edges.getSelectedEdges();
  889 + const connections: RuleNodeConnection[] = [];
  890 + edges.forEach((edge) => {
  891 + const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source);
  892 + const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination);
  893 + const isInputSource = sourceNode.component.type == RuleNodeType.INPUT;
  894 + const fromIndex = nodes.indexOf(sourceNode);
  895 + const toIndex = nodes.indexOf(destNode);
  896 + if ( (isInputSource || fromIndex > -1) && toIndex > -1 ) {
  897 + const connection: RuleNodeConnection = {
  898 + isInputSource: isInputSource,
  899 + fromIndex: fromIndex,
  900 + toIndex: toIndex,
  901 + label: edge.label,
  902 + labels: edge.labels
  903 + };
  904 + connections.push(connection);
  905 + }
  906 + });
  907 + this.itembuffer.copyRuleNodes(nodes, connections);
  908 + }
  909 +
  910 + private pasteRuleNodes(event?: MouseEvent) {
  911 + const canvas = $(this.ruleChainCanvas.modelService.canvasHtmlElement);
  912 + let x: number;
  913 + let y: number;
  914 + if (event) {
  915 + const offset = canvas.offset();
  916 + x = Math.round(event.clientX - offset.left);
  917 + y = Math.round(event.clientY - offset.top);
  918 + } else {
  919 + const scrollParent = canvas.parent();
  920 + const scrollTop = scrollParent.scrollTop();
  921 + const scrollLeft = scrollParent.scrollLeft();
  922 + x = scrollLeft + scrollParent.width()/2;
  923 + y = scrollTop + scrollParent.height()/2;
  924 + }
  925 + const ruleNodes = this.itembuffer.pasteRuleNodes(x, y);
  926 + if (ruleNodes) {
  927 + this.ruleChainCanvas.modelService.deselectAll();
  928 + const nodes: FcRuleNode[] = [];
  929 + ruleNodes.nodes.forEach((node) => {
  930 + node.id = 'rule-chain-node-' + this.nextNodeID++;
  931 + const component = node.component;
  932 + if (component.configurationDescriptor.nodeDefinition.inEnabled) {
  933 + node.connectors.push(
  934 + {
  935 + type: FlowchartConstants.leftConnectorType,
  936 + id: (this.nextConnectorID++) + ''
  937 + }
  938 + );
  939 + }
  940 + if (component.configurationDescriptor.nodeDefinition.outEnabled) {
  941 + node.connectors.push(
  942 + {
  943 + type: FlowchartConstants.rightConnectorType,
  944 + id: (this.nextConnectorID++) + ''
  945 + }
  946 + );
  947 + }
  948 + nodes.push(node);
  949 + this.ruleChainModel.nodes.push(node);
  950 + this.ruleChainCanvas.modelService.nodes.select(node);
  951 + });
  952 + ruleNodes.connections.forEach((connection) => {
  953 + const sourceNode = nodes[connection.fromIndex];
  954 + const destNode = nodes[connection.toIndex];
  955 + if ( (connection.isInputSource || sourceNode) && destNode ) {
  956 + let source: string;
  957 + let destination: string;
  958 + if (connection.isInputSource) {
  959 + source = this.inputConnectorId + '';
  960 + const found = this.ruleChainModel.edges.find(theEdge => theEdge.source === (this.inputConnectorId + ''));
  961 + if (found) {
  962 + this.ruleChainCanvas.modelService.edges.delete(found);
  963 + }
  964 + } else {
  965 + const sourceConnectors = this.ruleChainCanvas.modelService.nodes.getConnectorsByType(sourceNode, FlowchartConstants.rightConnectorType);
  966 + if (sourceConnectors && sourceConnectors.length) {
  967 + source = sourceConnectors[0].id;
  968 + }
  969 + }
  970 + const destConnectors = this.ruleChainCanvas.modelService.nodes.getConnectorsByType(destNode, FlowchartConstants.leftConnectorType);
  971 + if (destConnectors && destConnectors.length) {
  972 + destination = destConnectors[0].id;
  973 + }
  974 + if (source && destination) {
  975 + const edge: FcRuleEdge = {
  976 + source: source,
  977 + destination: destination,
  978 + label: connection.label,
  979 + labels: connection.labels
  980 + };
  981 + this.ruleChainModel.edges.push(edge);
  982 + this.ruleChainCanvas.modelService.edges.select(edge);
  983 + }
  984 + }
  985 + });
  986 + this.updateRuleNodesHighlight();
  987 + this.validate();
  988 + this.onModelChanged();
  989 + }
  990 + }
  991 +
561 onDetailsDrawerClosed() { 992 onDetailsDrawerClosed() {
562 this.onEditRuleNodeClosed(); 993 this.onEditRuleNodeClosed();
563 this.onEditRuleNodeLinkClosed(); 994 this.onEditRuleNodeLinkClosed();
  995 + this.enableHotKeys = true;
  996 + this.updateErrorTooltips(false);
564 } 997 }
565 998
566 onEditRuleNodeClosed() { 999 onEditRuleNodeClosed() {
@@ -739,6 +1172,7 @@ export class RuleChainPageComponent extends PageComponent @@ -739,6 +1172,7 @@ export class RuleChainPageComponent extends PageComponent
739 addRuleNode(ruleNode: FcRuleNode) { 1172 addRuleNode(ruleNode: FcRuleNode) {
740 ruleNode.configuration = deepClone(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration); 1173 ruleNode.configuration = deepClone(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
741 const ruleChainId = this.ruleChain.id ? this.ruleChain.id.id : null; 1174 const ruleChainId = this.ruleChain.id ? this.ruleChain.id.id : null;
  1175 + this.enableHotKeys = false;
742 this.dialog.open<AddRuleNodeDialogComponent, AddRuleNodeDialogData, 1176 this.dialog.open<AddRuleNodeDialogComponent, AddRuleNodeDialogData,
743 FcRuleNode>(AddRuleNodeDialogComponent, { 1177 FcRuleNode>(AddRuleNodeDialogComponent, {
744 disableClose: true, 1178 disableClose: true,
@@ -772,6 +1206,7 @@ export class RuleChainPageComponent extends PageComponent @@ -772,6 +1206,7 @@ export class RuleChainPageComponent extends PageComponent
772 this.onModelChanged(); 1206 this.onModelChanged();
773 this.updateRuleNodesHighlight(); 1207 this.updateRuleNodesHighlight();
774 } 1208 }
  1209 + this.enableHotKeys = true;
775 } 1210 }
776 ); 1211 );
777 } 1212 }
@@ -836,6 +1271,17 @@ export class RuleChainPageComponent extends PageComponent @@ -836,6 +1271,17 @@ export class RuleChainPageComponent extends PageComponent
836 } 1271 }
837 } 1272 }
838 1273
  1274 + private updateErrorTooltips(hide: boolean) {
  1275 + for (const nodeId of Object.keys(this.errorTooltips)) {
  1276 + const tooltip = this.errorTooltips[nodeId];
  1277 + if (hide) {
  1278 + tooltip.close();
  1279 + } else {
  1280 + tooltip.open();
  1281 + }
  1282 + }
  1283 + }
  1284 +
839 private displayTooltip(event: MouseEvent, content: string) { 1285 private displayTooltip(event: MouseEvent, content: string) {
840 this.destroyTooltips(); 1286 this.destroyTooltips();
841 this.tooltipTimeout = setTimeout(() => { 1287 this.tooltipTimeout = setTimeout(() => {
@@ -14,37 +14,32 @@ @@ -14,37 +14,32 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { FcNode, FcEdge, FcModel } from 'ngx-flowchart/dist/ngx-flowchart';  
18 -import { RuleNodeComponentDescriptor, RuleNodeConfiguration } from '@shared/models/rule-node.models';  
19 -import { RuleNodeId } from '@app/shared/models/id/rule-node-id';  
20 -import { RuleChainId } from '@shared/models/id/rule-chain-id';  
21 -  
22 -export interface FcRuleNodeType extends FcNode {  
23 - component: RuleNodeComponentDescriptor;  
24 - nodeClass: string;  
25 - icon: string;  
26 - iconUrl?: string;  
27 -} 17 +import { FcModel } from 'ngx-flowchart/dist/ngx-flowchart';
  18 +import { FcRuleEdge, FcRuleNode, FcRuleNodeType } from '@shared/models/rule-node.models';
28 19
29 export interface FcRuleNodeTypeModel extends FcModel { 20 export interface FcRuleNodeTypeModel extends FcModel {
30 nodes: Array<FcRuleNodeType>; 21 nodes: Array<FcRuleNodeType>;
31 } 22 }
32 23
33 -export interface FcRuleNode extends FcRuleNodeType {  
34 - ruleNodeId?: RuleNodeId;  
35 - additionalInfo?: any;  
36 - configuration?: RuleNodeConfiguration;  
37 - debugMode?: boolean;  
38 - targetRuleChainId?: string;  
39 - error?: string;  
40 - highlighted?: boolean; 24 +export interface FcRuleNodeModel extends FcModel {
  25 + nodes: Array<FcRuleNode>;
  26 + edges: Array<FcRuleEdge>;
41 } 27 }
42 28
43 -export interface FcRuleEdge extends FcEdge {  
44 - labels?: string[]; 29 +export interface RuleChainMenuItem {
  30 + action?: ($event: MouseEvent) => void;
  31 + enabled?: boolean;
  32 + value?: string;
  33 + icon?: string;
  34 + shortcut?: string;
  35 + divider?: boolean;
45 } 36 }
46 37
47 -export interface FcRuleNodeModel extends FcModel {  
48 - nodes: Array<FcRuleNode>;  
49 - edges: Array<FcRuleEdge>; 38 +export interface RuleChainMenuContextInfo {
  39 + headerClass: string;
  40 + icon: string;
  41 + iconUrl?: string;
  42 + title: string;
  43 + subtitle: string;
  44 + menuItems: RuleChainMenuItem[];
50 } 45 }
@@ -14,6 +14,8 @@ @@ -14,6 +14,8 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 16
  17 +@import './rule-node-colors';
  18 +
17 :host { 19 :host {
18 20
19 .fc-node-overlay { 21 .fc-node-overlay {
@@ -63,33 +65,7 @@ @@ -63,33 +65,7 @@
63 border: solid 1px #777; 65 border: solid 1px #777;
64 border-radius: 5px; 66 border-radius: 5px;
65 67
66 - &.tb-filter-type {  
67 - background-color: #f1e861;  
68 - }  
69 -  
70 - &.tb-enrichment-type {  
71 - background-color: #cdf14e;  
72 - }  
73 -  
74 - &.tb-transformation-type {  
75 - background-color: #79cef1;  
76 - }  
77 -  
78 - &.tb-action-type {  
79 - background-color: #f1928f;  
80 - }  
81 -  
82 - &.tb-external-type {  
83 - background-color: #fbc766;  
84 - }  
85 -  
86 - &.tb-rule-chain-type {  
87 - background-color: #d6c4f1;  
88 - }  
89 -  
90 - &.tb-unknown-type {  
91 - background-color: #f16c29;  
92 - } 68 + @include rule-node-colors();
93 69
94 &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) { 70 &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) {
95 box-shadow: 0 0 10px 6px #51cbee; 71 box-shadow: 0 0 10px 6px #51cbee;
@@ -15,9 +15,9 @@ @@ -15,9 +15,9 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<hotkeys-cheatsheet></hotkeys-cheatsheet>  
19 <div fxFlex fxLayout="column"> 18 <div fxFlex fxLayout="column">
20 - <div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen"> 19 + <div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen" tb-hotkeys [hotkeys]="hotKeys" [cheatSheet]="cheatSheetComponent">
  20 + <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
21 <mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px"> 21 <mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px">
22 <mat-form-field floatLabel="always" hideRequiredMarker class="tb-widget-title"> 22 <mat-form-field floatLabel="always" hideRequiredMarker class="tb-widget-title">
23 <mat-label></mat-label> 23 <mat-label></mat-label>
@@ -139,6 +139,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe @@ -139,6 +139,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
139 139
140 saveWidgetTimeout: Timeout; 140 saveWidgetTimeout: Timeout;
141 141
  142 + hotKeys: Hotkey[] = [];
  143 +
142 private rxSubscriptions = new Array<Subscription>(); 144 private rxSubscriptions = new Array<Subscription>();
143 145
144 constructor(protected store: Store<AppState>, 146 constructor(protected store: Store<AppState>,
@@ -146,7 +148,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe @@ -146,7 +148,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
146 private route: ActivatedRoute, 148 private route: ActivatedRoute,
147 private router: Router, 149 private router: Router,
148 private widgetService: WidgetService, 150 private widgetService: WidgetService,
149 - private hotkeysService: HotkeysService,  
150 private translate: TranslateService, 151 private translate: TranslateService,
151 private raf: RafService, 152 private raf: RafService,
152 private dialog: MatDialog) { 153 private dialog: MatDialog) {
@@ -159,6 +160,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe @@ -159,6 +160,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
159 this.init(data); 160 this.init(data);
160 } 161 }
161 )); 162 ));
  163 +
  164 + this.initHotKeys();
162 } 165 }
163 166
164 private init(data: any) { 167 private init(data: any) {
@@ -181,7 +184,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe @@ -181,7 +184,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
181 } 184 }
182 185
183 ngOnInit(): void { 186 ngOnInit(): void {
184 - this.initHotKeys();  
185 this.initSplitLayout(); 187 this.initSplitLayout();
186 this.initAceEditors(); 188 this.initAceEditors();
187 this.iframe = $(this.widgetIFrameElmRef.nativeElement); 189 this.iframe = $(this.widgetIFrameElmRef.nativeElement);
@@ -203,7 +205,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe @@ -203,7 +205,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
203 } 205 }
204 206
205 private initHotKeys(): void { 207 private initHotKeys(): void {
206 - this.hotkeysService.add( 208 + this.hotKeys.push(
207 new Hotkey('ctrl+q', (event: KeyboardEvent) => { 209 new Hotkey('ctrl+q', (event: KeyboardEvent) => {
208 if (!getCurrentIsLoading(this.store) && !this.undoDisabled()) { 210 if (!getCurrentIsLoading(this.store) && !this.undoDisabled()) {
209 event.preventDefault(); 211 event.preventDefault();
@@ -213,7 +215,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe @@ -213,7 +215,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
213 }, ['INPUT', 'SELECT', 'TEXTAREA'], 215 }, ['INPUT', 'SELECT', 'TEXTAREA'],
214 this.translate.instant('widget.undo')) 216 this.translate.instant('widget.undo'))
215 ); 217 );
216 - this.hotkeysService.add( 218 + this.hotKeys.push(
217 new Hotkey('ctrl+s', (event: KeyboardEvent) => { 219 new Hotkey('ctrl+s', (event: KeyboardEvent) => {
218 if (!getCurrentIsLoading(this.store) && !this.saveDisabled()) { 220 if (!getCurrentIsLoading(this.store) && !this.saveDisabled()) {
219 event.preventDefault(); 221 event.preventDefault();
@@ -223,7 +225,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe @@ -223,7 +225,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
223 }, ['INPUT', 'SELECT', 'TEXTAREA'], 225 }, ['INPUT', 'SELECT', 'TEXTAREA'],
224 this.translate.instant('widget.save')) 226 this.translate.instant('widget.save'))
225 ); 227 );
226 - this.hotkeysService.add( 228 + this.hotKeys.push(
227 new Hotkey('shift+ctrl+s', (event: KeyboardEvent) => { 229 new Hotkey('shift+ctrl+s', (event: KeyboardEvent) => {
228 if (!getCurrentIsLoading(this.store) && !this.saveAsDisabled()) { 230 if (!getCurrentIsLoading(this.store) && !this.saveAsDisabled()) {
229 event.preventDefault(); 231 event.preventDefault();
@@ -233,7 +235,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe @@ -233,7 +235,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
233 }, ['INPUT', 'SELECT', 'TEXTAREA'], 235 }, ['INPUT', 'SELECT', 'TEXTAREA'],
234 this.translate.instant('widget.saveAs')) 236 this.translate.instant('widget.saveAs'))
235 ); 237 );
236 - this.hotkeysService.add( 238 + this.hotKeys.push(
237 new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => { 239 new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => {
238 event.preventDefault(); 240 event.preventDefault();
239 this.fullscreen = !this.fullscreen; 241 this.fullscreen = !this.fullscreen;
@@ -241,7 +243,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe @@ -241,7 +243,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
241 }, ['INPUT', 'SELECT', 'TEXTAREA'], 243 }, ['INPUT', 'SELECT', 'TEXTAREA'],
242 this.translate.instant('widget.toggle-fullscreen')) 244 this.translate.instant('widget.toggle-fullscreen'))
243 ); 245 );
244 - this.hotkeysService.add( 246 + this.hotKeys.push(
245 new Hotkey('ctrl+enter', (event: KeyboardEvent) => { 247 new Hotkey('ctrl+enter', (event: KeyboardEvent) => {
246 event.preventDefault(); 248 event.preventDefault();
247 this.applyWidgetScript(); 249 this.applyWidgetScript();
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
  18 +import { Hotkey, HotkeysService } from 'angular2-hotkeys';
  19 +
  20 +@Component({
  21 + selector : 'tb-hotkeys-cheatsheet',
  22 + styles : [`
  23 +.tb-hotkeys-container {
  24 + display: table !important;
  25 + position: fixed;
  26 + width: 100%;
  27 + height: 100%;
  28 + top: 0;
  29 + left: 0;
  30 + color: #333;
  31 + font-size: 1em;
  32 + background-color: rgba(255,255,255,0.9);
  33 + outline: 0;
  34 +}
  35 +.tb-hotkeys-container.fade {
  36 + z-index: -1024;
  37 + visibility: hidden;
  38 + opacity: 0;
  39 + -webkit-transition: opacity 0.15s linear;
  40 + -moz-transition: opacity 0.15s linear;
  41 + -o-transition: opacity 0.15s linear;
  42 + transition: opacity 0.15s linear;
  43 +}
  44 +.tb-hotkeys-container.fade.in {
  45 + z-index: 10002;
  46 + visibility: visible;
  47 + opacity: 1;
  48 +}
  49 +.tb-hotkeys-title {
  50 + font-weight: bold;
  51 + text-align: center;
  52 + font-size: 1.2em;
  53 +}
  54 +.tb-hotkeys {
  55 + width: 100%;
  56 + height: 100%;
  57 + display: table-cell;
  58 + vertical-align: middle;
  59 +}
  60 +.tb-hotkeys table {
  61 + margin: auto;
  62 + color: #333;
  63 +}
  64 +.tb-content {
  65 + display: table-cell;
  66 + vertical-align: middle;
  67 +}
  68 +.tb-hotkeys-keys {
  69 + padding: 5px;
  70 + text-align: right;
  71 +}
  72 +.tb-hotkeys-key {
  73 + display: inline-block;
  74 + color: #fff;
  75 + background-color: #333;
  76 + border: 1px solid #333;
  77 + border-radius: 5px;
  78 + text-align: center;
  79 + margin-right: 5px;
  80 + box-shadow: inset 0 1px 0 #666, 0 1px 0 #bbb;
  81 + padding: 5px 9px;
  82 + font-size: 1em;
  83 +}
  84 +.tb-hotkeys-text {
  85 + padding-left: 10px;
  86 + font-size: 1em;
  87 +}
  88 +.tb-hotkeys-close {
  89 + position: fixed;
  90 + top: 20px;
  91 + right: 20px;
  92 + font-size: 2em;
  93 + font-weight: bold;
  94 + padding: 5px 10px;
  95 + border: 1px solid #ddd;
  96 + border-radius: 5px;
  97 + min-height: 45px;
  98 + min-width: 45px;
  99 + text-align: center;
  100 +}
  101 +.tb-hotkeys-close:hover {
  102 + background-color: #fff;
  103 + cursor: pointer;
  104 +}
  105 +@media all and (max-width: 500px) {
  106 + .tb-hotkeys {
  107 + font-size: 0.8em;
  108 + }
  109 +}
  110 +@media all and (min-width: 750px) {
  111 + .tb-hotkeys {
  112 + font-size: 1.2em;
  113 + }
  114 +} `],
  115 + template : `<div tabindex="-1" class="tb-hotkeys-container fade" [ngClass]="{'in': helpVisible}" style="display:none"><div class="tb-hotkeys">
  116 + <h4 class="tb-hotkeys-title">{{ title }}</h4>
  117 + <table *ngIf="helpVisible"><tbody>
  118 + <tr *ngFor="let hotkey of hotkeysList">
  119 + <td class="tb-hotkeys-keys">
  120 + <span *ngFor="let key of hotkey.formatted" class="tb-hotkeys-key">{{ key }}</span>
  121 + </td>
  122 + <td class="tb-hotkeys-text">{{ hotkey.description }}</td>
  123 + </tr>
  124 + </tbody></table>
  125 + <div class="tb-hotkeys-close" (click)="toggleCheatSheet()">&#215;</div>
  126 +</div></div>`,
  127 +})
  128 +export class TbCheatSheetComponent implements OnInit, OnDestroy {
  129 +
  130 + helpVisible = false;
  131 + @Input() title: string = 'Keyboard Shortcuts:';
  132 +
  133 + @Input()
  134 + hotkeys: Hotkey[];
  135 +
  136 + hotkeysList: Hotkey[];
  137 +
  138 + private mousetrap: MousetrapInstance;
  139 +
  140 + constructor(private _elementRef: ElementRef,
  141 + private hotkeysService: HotkeysService) {
  142 + this.mousetrap = new Mousetrap(this._elementRef.nativeElement);
  143 + this.mousetrap.bind('?', (event: KeyboardEvent, combo: string) => {
  144 + this.toggleCheatSheet();
  145 + });
  146 + }
  147 +
  148 + public ngOnInit(): void {
  149 + if (this.hotkeys) {
  150 + this.hotkeysList = this.hotkeys.filter(hotkey => hotkey.description);
  151 + }
  152 + }
  153 +
  154 + public setHotKeys(hotkeys: Hotkey[]) {
  155 + this.hotkeysList = hotkeys.filter(hotkey => hotkey.description);
  156 + }
  157 +
  158 + public toggleCheatSheet(): void {
  159 + this.helpVisible = !this.helpVisible;
  160 + }
  161 +
  162 + ngOnDestroy() {
  163 + this.mousetrap.unbind('?');
  164 + }
  165 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {Directive, Input, OnInit, OnDestroy, ElementRef} from '@angular/core';
  18 +import {Hotkey, ExtendedKeyboardEvent} from 'angular2-hotkeys';
  19 +import 'mousetrap';
  20 +import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component';
  21 +
  22 +@Directive({
  23 + selector : '[tb-hotkeys]'
  24 +})
  25 +export class TbHotkeysDirective implements OnInit, OnDestroy {
  26 + @Input() hotkeys: Hotkey[] = [];
  27 + @Input() cheatSheet: TbCheatSheetComponent;
  28 +
  29 + private mousetrap: MousetrapInstance;
  30 + private hotkeysList: Hotkey[] = [];
  31 +
  32 + private _preventIn = ['INPUT', 'SELECT', 'TEXTAREA'];
  33 +
  34 + constructor(private _elementRef: ElementRef) {
  35 + this.mousetrap = new Mousetrap(this._elementRef.nativeElement);
  36 + (this._elementRef.nativeElement as HTMLElement).tabIndex = -1;
  37 + (this._elementRef.nativeElement as HTMLElement).style.outline = '0';
  38 + }
  39 +
  40 + ngOnInit() {
  41 + for (let hotkey of this.hotkeys) {
  42 + this.hotkeysList.push(hotkey);
  43 + this.bindEvent(hotkey);
  44 + }
  45 + if (this.cheatSheet) {
  46 + let hotkeyObj: Hotkey = new Hotkey(
  47 + '?',
  48 + (event: KeyboardEvent) => {
  49 + this.cheatSheet.toggleCheatSheet();
  50 + return false;
  51 + },
  52 + [],
  53 + 'Show / hide this help menu',
  54 + );
  55 + this.hotkeysList.unshift(hotkeyObj);
  56 + this.bindEvent(hotkeyObj);
  57 + this.cheatSheet.setHotKeys(this.hotkeysList);
  58 + }
  59 + }
  60 +
  61 + private bindEvent(hotkey: Hotkey): void {
  62 + this.mousetrap.bind((<Hotkey>hotkey).combo, (event: KeyboardEvent, combo: string) => {
  63 + let shouldExecute = true;
  64 + if(event) {
  65 + let target: HTMLElement = <HTMLElement>(event.target || event.srcElement);
  66 + let nodeName: string = target.nodeName.toUpperCase();
  67 + if((' ' + target.className + ' ').indexOf(' mousetrap ') > -1) {
  68 + shouldExecute = true;
  69 + } else if(this._preventIn.indexOf(nodeName) > -1 && (<Hotkey>hotkey).allowIn.map(allow => allow.toUpperCase()).indexOf(nodeName) === -1) {
  70 + shouldExecute = false;
  71 + }
  72 + }
  73 +
  74 + if(shouldExecute) {
  75 + return (<Hotkey>hotkey).callback.apply(this, [event, combo]);
  76 + }
  77 + });
  78 + }
  79 +
  80 + ngOnDestroy() {
  81 + for (let hotkey of this.hotkeysList) {
  82 + this.mousetrap.unbind(hotkey.combo);
  83 + }
  84 + }
  85 +
  86 +}
@@ -14,18 +14,14 @@ @@ -14,18 +14,14 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import {BaseData} from '@shared/models/base-data';  
18 -import {AssetId} from '@shared/models/id/asset-id';  
19 -import {TenantId} from '@shared/models/id/tenant-id';  
20 -import {CustomerId} from '@shared/models/id/customer-id';  
21 -import {RuleChainId} from '@shared/models/id/rule-chain-id';  
22 -import {RuleNodeId} from '@shared/models/id/rule-node-id';  
23 -import { ComponentDescriptor, ComponentType } from '@shared/models/component-descriptor.models';  
24 -import { EntityType, EntityTypeResource } from '@shared/models/entity-type.models'; 17 +import { BaseData } from '@shared/models/base-data';
  18 +import { RuleChainId } from '@shared/models/id/rule-chain-id';
  19 +import { RuleNodeId } from '@shared/models/id/rule-node-id';
  20 +import { ComponentDescriptor } from '@shared/models/component-descriptor.models';
  21 +import { FcEdge, FcNode } from 'ngx-flowchart/dist/ngx-flowchart';
25 import { Observable } from 'rxjs'; 22 import { Observable } from 'rxjs';
26 import { PageComponent } from '@shared/components/page.component'; 23 import { PageComponent } from '@shared/components/page.component';
27 -import { AfterViewInit, ComponentFactory, EventEmitter, Inject, OnDestroy, OnInit } from '@angular/core';  
28 -import { RafService } from '@core/services/raf.service'; 24 +import { AfterViewInit, EventEmitter, Inject, OnInit } from '@angular/core';
29 import { Store } from '@ngrx/store'; 25 import { Store } from '@ngrx/store';
30 import { AppState } from '@core/core.state'; 26 import { AppState } from '@core/core.state';
31 import { AbstractControl, FormGroup } from '@angular/forms'; 27 import { AbstractControl, FormGroup } from '@angular/forms';
@@ -38,7 +34,6 @@ export enum MsgDataType { @@ -38,7 +34,6 @@ export enum MsgDataType {
38 34
39 export interface RuleNodeConfiguration { 35 export interface RuleNodeConfiguration {
40 [key: string]: any; 36 [key: string]: any;
41 - // TODO:  
42 } 37 }
43 38
44 export interface RuleNode extends BaseData<RuleNodeId> { 39 export interface RuleNode extends BaseData<RuleNodeId> {
@@ -307,6 +302,28 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor { @@ -307,6 +302,28 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor {
307 configurationDescriptor?: RuleNodeConfigurationDescriptor; 302 configurationDescriptor?: RuleNodeConfigurationDescriptor;
308 } 303 }
309 304
  305 +export interface FcRuleNodeType extends FcNode {
  306 + component?: RuleNodeComponentDescriptor;
  307 + nodeClass?: string;
  308 + icon?: string;
  309 + iconUrl?: string;
  310 +}
  311 +
  312 +export interface FcRuleNode extends FcRuleNodeType {
  313 + ruleNodeId?: RuleNodeId;
  314 + additionalInfo?: any;
  315 + configuration?: RuleNodeConfiguration;
  316 + debugMode?: boolean;
  317 + targetRuleChainId?: string;
  318 + error?: string;
  319 + highlighted?: boolean;
  320 + componentClazz?: string;
  321 +}
  322 +
  323 +export interface FcRuleEdge extends FcEdge {
  324 + labels?: string[];
  325 +}
  326 +
310 export interface TestScriptInputParams { 327 export interface TestScriptInputParams {
311 script: string; 328 script: string;
312 scriptType: string; 329 scriptType: string;
@@ -118,6 +118,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc @@ -118,6 +118,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
118 import { MessageTypeAutocompleteComponent } from './components/message-type-autocomplete.component'; 118 import { MessageTypeAutocompleteComponent } from './components/message-type-autocomplete.component';
119 import { JsonContentComponent } from './components/json-content.component'; 119 import { JsonContentComponent } from './components/json-content.component';
120 import { KeyValMapComponent } from './components/kv-map.component'; 120 import { KeyValMapComponent } from './components/kv-map.component';
  121 +import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component';
  122 +import { TbHotkeysDirective } from '@shared/components/hotkeys.directive';
121 123
122 @NgModule({ 124 @NgModule({
123 providers: [ 125 providers: [
@@ -149,11 +151,13 @@ import { KeyValMapComponent } from './components/kv-map.component'; @@ -149,11 +151,13 @@ import { KeyValMapComponent } from './components/kv-map.component';
149 FullscreenDirective, 151 FullscreenDirective,
150 CircularProgressDirective, 152 CircularProgressDirective,
151 MatChipDraggableDirective, 153 MatChipDraggableDirective,
  154 + TbHotkeysDirective,
152 TbAnchorComponent, 155 TbAnchorComponent,
153 HelpComponent, 156 HelpComponent,
154 TbCheckboxComponent, 157 TbCheckboxComponent,
155 TbSnackBarComponent, 158 TbSnackBarComponent,
156 TbErrorComponent, 159 TbErrorComponent,
  160 + TbCheatSheetComponent,
157 BreadcrumbComponent, 161 BreadcrumbComponent,
158 UserMenuComponent, 162 UserMenuComponent,
159 TimewindowComponent, 163 TimewindowComponent,
@@ -256,10 +260,12 @@ import { KeyValMapComponent } from './components/kv-map.component'; @@ -256,10 +260,12 @@ import { KeyValMapComponent } from './components/kv-map.component';
256 FullscreenDirective, 260 FullscreenDirective,
257 CircularProgressDirective, 261 CircularProgressDirective,
258 MatChipDraggableDirective, 262 MatChipDraggableDirective,
  263 + TbHotkeysDirective,
259 TbAnchorComponent, 264 TbAnchorComponent,
260 HelpComponent, 265 HelpComponent,
261 TbCheckboxComponent, 266 TbCheckboxComponent,
262 TbErrorComponent, 267 TbErrorComponent,
  268 + TbCheatSheetComponent,
263 BreadcrumbComponent, 269 BreadcrumbComponent,
264 UserMenuComponent, 270 UserMenuComponent,
265 TimewindowComponent, 271 TimewindowComponent,