Commit 53b6aeb4fa1b9a976affa96d66563d7d6a939d12

Authored by Igor Kulikov
1 parent 08e5a7e0

Rule chain page. Inprove hotkeys handling

... ... @@ -1407,14 +1407,12 @@
1407 1407 "balanced-match": {
1408 1408 "version": "1.0.0",
1409 1409 "bundled": true,
1410   - "dev": true,
1411   - "optional": true
  1410 + "dev": true
1412 1411 },
1413 1412 "brace-expansion": {
1414 1413 "version": "1.1.11",
1415 1414 "bundled": true,
1416 1415 "dev": true,
1417   - "optional": true,
1418 1416 "requires": {
1419 1417 "balanced-match": "^1.0.0",
1420 1418 "concat-map": "0.0.1"
... ... @@ -1429,20 +1427,17 @@
1429 1427 "code-point-at": {
1430 1428 "version": "1.1.0",
1431 1429 "bundled": true,
1432   - "dev": true,
1433   - "optional": true
  1430 + "dev": true
1434 1431 },
1435 1432 "concat-map": {
1436 1433 "version": "0.0.1",
1437 1434 "bundled": true,
1438   - "dev": true,
1439   - "optional": true
  1435 + "dev": true
1440 1436 },
1441 1437 "console-control-strings": {
1442 1438 "version": "1.1.0",
1443 1439 "bundled": true,
1444   - "dev": true,
1445   - "optional": true
  1440 + "dev": true
1446 1441 },
1447 1442 "core-util-is": {
1448 1443 "version": "1.0.2",
... ... @@ -1559,8 +1554,7 @@
1559 1554 "inherits": {
1560 1555 "version": "2.0.3",
1561 1556 "bundled": true,
1562   - "dev": true,
1563   - "optional": true
  1557 + "dev": true
1564 1558 },
1565 1559 "ini": {
1566 1560 "version": "1.3.5",
... ... @@ -1572,7 +1566,6 @@
1572 1566 "version": "1.0.0",
1573 1567 "bundled": true,
1574 1568 "dev": true,
1575   - "optional": true,
1576 1569 "requires": {
1577 1570 "number-is-nan": "^1.0.0"
1578 1571 }
... ... @@ -1587,7 +1580,6 @@
1587 1580 "version": "3.0.4",
1588 1581 "bundled": true,
1589 1582 "dev": true,
1590   - "optional": true,
1591 1583 "requires": {
1592 1584 "brace-expansion": "^1.1.7"
1593 1585 }
... ... @@ -1699,8 +1691,7 @@
1699 1691 "number-is-nan": {
1700 1692 "version": "1.0.1",
1701 1693 "bundled": true,
1702   - "dev": true,
1703   - "optional": true
  1694 + "dev": true
1704 1695 },
1705 1696 "object-assign": {
1706 1697 "version": "4.1.1",
... ... @@ -1712,7 +1703,6 @@
1712 1703 "version": "1.4.0",
1713 1704 "bundled": true,
1714 1705 "dev": true,
1715   - "optional": true,
1716 1706 "requires": {
1717 1707 "wrappy": "1"
1718 1708 }
... ... @@ -1834,7 +1824,6 @@
1834 1824 "version": "1.0.2",
1835 1825 "bundled": true,
1836 1826 "dev": true,
1837   - "optional": true,
1838 1827 "requires": {
1839 1828 "code-point-at": "^1.0.0",
1840 1829 "is-fullwidth-code-point": "^1.0.0",
... ...
... ... @@ -186,7 +186,9 @@ export class AliasController implements IAliasController {
186 186 );
187 187 } else {
188 188 resolvedAliasSubject.error(null);
  189 + const res = this.resolvedAliasesObservable[aliasId];
189 190 delete this.resolvedAliasesObservable[aliasId];
  191 + return res;
190 192 }
191 193 return this.resolvedAliasesObservable[aliasId];
192 194 }
... ...
... ... @@ -24,6 +24,8 @@ import * as equal from 'deep-equal';
24 24 import { UtilsService } from '@core/services/utils.service';
25 25 import { Observable, of, throwError } from 'rxjs';
26 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 30 const WIDGET_ITEM = 'widget_item';
29 31 const WIDGET_REFERENCE = 'widget_reference';
... ... @@ -45,6 +47,21 @@ export interface WidgetReference {
45 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 65 @Injectable({
49 66 providedIn: 'root'
50 67 })
... ... @@ -54,6 +71,7 @@ export class ItemBufferService {
54 71 private delimiter = '.';
55 72
56 73 constructor(private dashboardUtils: DashboardUtilsService,
  74 + private ruleChainService: RuleChainService,
57 75 private utils: UtilsService) {}
58 76
59 77 public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetItem {
... ... @@ -99,12 +117,12 @@ export class ItemBufferService {
99 117
100 118 public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void {
101 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 123 public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void {
106 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 128 public hasWidget(): boolean {
... ... @@ -112,9 +130,8 @@ export class ItemBufferService {
112 130 }
113 131
114 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 135 if (widgetReference.dashboardId === dashboard.id.id) {
119 136 if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout)
120 137 && dashboard.configuration.widgets[widgetReference.widgetId]) {
... ... @@ -128,9 +145,8 @@ export class ItemBufferService {
128 145 public pasteWidget(targetDashboard: Dashboard, targetState: string,
129 146 targetLayout: DashboardLayoutId, position: WidgetPosition,
130 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 150 const widget = widgetItem.widget;
135 151 const aliasesInfo = widgetItem.aliasesInfo;
136 152 const originalColumns = widgetItem.originalColumns;
... ... @@ -155,9 +171,8 @@ export class ItemBufferService {
155 171
156 172 public pasteWidgetReference(targetDashboard: Dashboard, targetState: string,
157 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 176 const widget = targetDashboard.configuration.widgets[widgetReference.widgetId];
162 177 if (widget) {
163 178 const originalColumns = widgetReference.originalColumns;
... ... @@ -216,6 +231,89 @@ export class ItemBufferService {
216 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 317 private getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId): number {
220 318 let originalColumns = 24;
221 319 let gridSettings = null;
... ...
... ... @@ -737,18 +737,22 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
737 737 dataLoading: (subscription) => {
738 738 if (this.loadingData !== subscription.loadingData) {
739 739 this.loadingData = subscription.loadingData;
740   - this.cd.detectChanges();
  740 + if (!this.destroyed) {
  741 + this.cd.detectChanges();
  742 + }
741 743 }
742 744 },
743 745 legendDataUpdated: (subscription, detectChanges) => {
744   - if (detectChanges) {
  746 + if (detectChanges && !this.destroyed) {
745 747 this.cd.detectChanges();
746 748 }
747 749 },
748 750 timeWindowUpdated: (subscription, timeWindowConfig) => {
749 751 this.ngZone.run(() => {
750 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 17 -->
18 18 <div class="tb-dashboard-page mat-content" style="padding-top: 150px;"
19 19 fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
  20 + <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
20 21 <section class="tb-dashboard-toolbar"
21 22 [ngClass]="{ 'tb-dashboard-toolbar-opened': toolbarOpened,
22 23 'tb-dashboard-toolbar-closed': !toolbarOpened }">
... ... @@ -146,6 +147,7 @@
146 147 [mode]="isMobile ? 'over' : 'side'"
147 148 [(opened)]="rightLayoutOpened">
148 149 <tb-dashboard-layout style="height: 100%;"
  150 + [dashboardCheatSheet]="cheatSheetComponent"
149 151 [layoutCtx]="layouts.right.layoutCtx"
150 152 [dashboardCtx]="dashboardCtx"
151 153 [isEdit]="isEdit"
... ... @@ -159,6 +161,7 @@
159 161 [ngStyle]="{width: mainLayoutWidth(),
160 162 height: mainLayoutHeight()}">
161 163 <tb-dashboard-layout
  164 + [dashboardCheatSheet]="cheatSheetComponent"
162 165 [layoutCtx]="layouts.main.layoutCtx"
163 166 [dashboardCtx]="dashboardCtx"
164 167 [isEdit]="isEdit"
... ...
... ... @@ -15,13 +15,14 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<hotkeys-cheatsheet></hotkeys-cheatsheet>
19 18 <div fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center"
20 19 *ngIf="layoutCtx.widgets.isLoading()">
21 20 <mat-spinner color="warn" mode="indeterminate" diameter="100">
22 21 </mat-spinner>
23 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 26 [style.backgroundImage]="backgroundImage"
26 27 [ngStyle]="dashboardStyle">
27 28 <section *ngIf="layoutCtx.widgets.isEmpty()" fxLayoutAlign="center center"
... ...
... ... @@ -14,27 +14,25 @@
14 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 18 import { ILayoutController } from '@home/pages/dashboard/layout/layout.models';
20 19 import { DashboardContext, DashboardPageLayoutContext } from '@home/pages/dashboard/dashboard-page.models';
21 20 import { PageComponent } from '@shared/components/page.component';
22 21 import { Store } from '@ngrx/store';
23 22 import { AppState } from '@core/core.state';
24 23 import { Widget } from '@shared/models/widget.models';
25   -import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
26   -import { GridsterComponent } from 'angular-gridster2';
27 24 import {
28 25 DashboardCallbacks,
29 26 DashboardContextMenuItem,
30   - IDashboardComponent, WidgetContextMenuItem
  27 + IDashboardComponent,
  28 + WidgetContextMenuItem
31 29 } from '@home/models/dashboard-component.models';
32   -import { Observable, of, Subscription } from 'rxjs';
  30 +import { Subscription } from 'rxjs';
33 31 import { Hotkey, HotkeysService } from 'angular2-hotkeys';
34   -import { getCurrentIsLoading } from '@core/interceptors/load.selectors';
35 32 import { TranslateService } from '@ngx-translate/core';
36 33 import { ItemBufferService } from '@app/core/services/item-buffer.service';
37 34 import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
  35 +import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component';
38 36
39 37 @Component({
40 38 selector: 'tb-dashboard-layout',
... ... @@ -47,6 +45,10 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
47 45 dashboardStyle: {[klass: string]: any} = null;
48 46 backgroundImage: SafeStyle | string;
49 47
  48 + hotKeys: Hotkey[] = [];
  49 +
  50 + @Input() dashboardCheatSheet: TbCheatSheetComponent;
  51 +
50 52 @Input()
51 53 set layoutCtx(val: DashboardPageLayoutContext) {
52 54 this.layoutCtxValue = val;
... ... @@ -81,11 +83,11 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
81 83 private rxSubscriptions = new Array<Subscription>();
82 84
83 85 constructor(protected store: Store<AppState>,
84   - private hotkeysService: HotkeysService,
85 86 private translate: TranslateService,
86 87 private itembuffer: ItemBufferService,
87 88 private sanitizer: DomSanitizer) {
88 89 super(store);
  90 + this.initHotKeys();
89 91 }
90 92
91 93 ngOnInit(): void {
... ... @@ -95,7 +97,6 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
95 97 this.dashboardCtx.runChangeDetection();
96 98 })
97 99 );
98   - this.initHotKeys();
99 100 }
100 101
101 102 ngOnDestroy(): void {
... ... @@ -106,7 +107,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
106 107 }
107 108
108 109 private initHotKeys(): void {
109   - this.hotkeysService.add(
  110 + this.hotKeys.push(
110 111 new Hotkey('ctrl+c', (event: KeyboardEvent) => {
111 112 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
112 113 const widget = this.dashboard.getSelectedWidget();
... ... @@ -119,7 +120,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
119 120 }, null,
120 121 this.translate.instant('action.copy'))
121 122 );
122   - this.hotkeysService.add(
  123 + this.hotKeys.push(
123 124 new Hotkey('ctrl+r', (event: KeyboardEvent) => {
124 125 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
125 126 const widget = this.dashboard.getSelectedWidget();
... ... @@ -132,7 +133,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
132 133 }, null,
133 134 this.translate.instant('action.copy-reference'))
134 135 );
135   - this.hotkeysService.add(
  136 + this.hotKeys.push(
136 137 new Hotkey('ctrl+v', (event: KeyboardEvent) => {
137 138 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
138 139 if (this.itembuffer.hasWidget()) {
... ... @@ -144,7 +145,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
144 145 }, null,
145 146 this.translate.instant('action.paste'))
146 147 );
147   - this.hotkeysService.add(
  148 + this.hotKeys.push(
148 149 new Hotkey('ctrl+i', (event: KeyboardEvent) => {
149 150 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
150 151 if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(),
... ... @@ -157,7 +158,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
157 158 }, null,
158 159 this.translate.instant('action.paste-reference'))
159 160 );
160   - this.hotkeysService.add(
  161 + this.hotKeys.push(
161 162 new Hotkey('ctrl+x', (event: KeyboardEvent) => {
162 163 if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
163 164 const widget = this.dashboard.getSelectedWidget();
... ...
... ... @@ -14,31 +14,14 @@
14 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 21 import { coerceBooleanProperty } from '@angular/cdk/coercion';
38 22 import { deepClone } from '@core/utils';
39   -import { EntityAlias } from '@shared/models/alias.models';
40 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 25 import { TranslateService } from '@ngx-translate/core';
43 26 import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
44 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 19 import { Store } from '@ngrx/store';
20 20 import { AppState } from '@core/core.state';
21 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 23 import { EntityType } from '@shared/models/entity-type.models';
25 24 import { Subscription } from 'rxjs';
26 25 import { RuleChainService } from '@core/http/rule-chain.service';
27   -import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component';
28 26 import { RuleNodeConfigComponent } from './rule-node-config.component';
29 27
30 28 @Component({
... ...
... ... @@ -14,34 +14,12 @@
14 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 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 20 import { coerceBooleanProperty } from '@angular/cdk/coercion';
38   -import { deepClone } from '@core/utils';
39   -import { EntityAlias } from '@shared/models/alias.models';
40 21 import { TruncatePipe } from '@shared/pipe/truncate.pipe';
41   -import { MatChipList, MatAutocomplete, MatChipInputEvent, MatAutocompleteSelectedEvent } from '@angular/material';
42 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 24 @Component({
47 25 selector: 'tb-rule-node-link',
... ...
... ... @@ -15,8 +15,10 @@
15 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 20 fxLayout="column" class="tb-rulechain">
  21 + <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
20 22 <section class="tb-rulechain-container" fxFlex fxLayout="column">
21 23 <div class="tb-rulechain-layout" fxFlex fxLayout="row">
22 24 <section fxLayout="row"
... ... @@ -80,6 +82,7 @@
80 82 [model]="ruleNodeTypesModel[ruleNodeType].model"
81 83 [selectedObjects]="ruleNodeTypesModel[ruleNodeType].selectedObjects"
82 84 [automaticResize]="false"
  85 + fitModelSizeByDefault
83 86 [userCallbacks]="nodeLibCallbacks"
84 87 [nodeWidth]="170"
85 88 [nodeHeight]="50"
... ... @@ -162,7 +165,38 @@
162 165 matTooltipPosition="above">
163 166 <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
164 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 200 <fc-canvas #ruleChainCanvas
167 201 id="tb-rulchain-canvas"
168 202 [model]="ruleChainModel"
... ... @@ -170,6 +204,7 @@
170 204 [selectedObjects]="selectedObjects"
171 205 [edgeStyle]="flowchartConstants.curvedStyle"
172 206 [automaticResize]="true"
  207 + fitModelSizeByDefault="false"
173 208 [nodeWidth]="170"
174 209 [nodeHeight]="50"
175 210 [dragAnimation]="flowchartConstants.dragAnimationRepaint"
... ...
... ... @@ -14,6 +14,8 @@
14 14 * limitations under the License.
15 15 */
16 16
  17 +@import './rule-node-colors';
  18 +
17 19 .tb-rulechain {
18 20 width: 100%;
19 21 height: 100%;
... ... @@ -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 18 AfterViewInit,
19 19 Component,
20 20 ElementRef,
21   - HostBinding, Inject,
  21 + HostBinding,
  22 + Inject,
22 23 OnInit,
23   - QueryList, SkipSelf,
  24 + QueryList,
  25 + SkipSelf,
24 26 ViewChild,
25 27 ViewChildren,
26 28 ViewEncapsulation
... ... @@ -31,7 +33,7 @@ import { AppState } from '@core/core.state';
31 33 import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
32 34 import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
33 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 37 import { DialogService } from '@core/services/dialog.service';
36 38 import { AuthService } from '@core/auth/auth.service';
37 39 import { ActivatedRoute, Router } from '@angular/router';
... ... @@ -41,8 +43,11 @@ import {
41 43 RuleChain,
42 44 ruleChainNodeComponent
43 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 47 import {
  48 + FcRuleEdge,
  49 + FcRuleNode,
  50 + FcRuleNodeType,
46 51 getRuleNodeHelpLink,
47 52 LinkLabel,
48 53 RuleNodeComponentDescriptor,
... ... @@ -50,24 +55,19 @@ import {
50 55 ruleNodeTypeDescriptors,
51 56 ruleNodeTypesLibrary
52 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 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 62 import { ISearchableComponent } from '../../models/searchable-component.models';
58   -import { deepClone, isDefined, isString } from '@core/utils';
  63 +import { deepClone } from '@core/utils';
59 64 import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-details.component';
60 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 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 72 @Component({
73 73 selector: 'tb-rulechain-page',
... ... @@ -92,6 +92,12 @@ export class RuleChainPageComponent extends PageComponent
92 92 @ViewChildren('ruleNodeTypeExpansionPanels',
93 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 101 ruleNodeTypeDescriptorsMap = ruleNodeTypeDescriptors;
96 102 ruleNodeTypesLibraryArray = ruleNodeTypesLibrary;
97 103
... ... @@ -116,6 +122,9 @@ export class RuleChainPageComponent extends PageComponent
116 122 isEditingRuleNodeLink = false;
117 123 editingRuleNodeLinkIndex = -1;
118 124
  125 + hotKeys: Hotkey[] = [];
  126 +
  127 + enableHotKeys = true;
119 128 isLibraryOpen = true;
120 129
121 130 ruleNodeSearch = '';
... ... @@ -173,7 +182,11 @@ export class RuleChainPageComponent extends PageComponent
173 182 } else {
174 183 const labels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
175 184 const allowCustomLabels = this.ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component);
  185 + this.enableHotKeys = false;
176 186 return this.addRuleNodeLink(edge, labels, allowCustomLabels).pipe(
  187 + tap(() => {
  188 + this.enableHotKeys = true;
  189 + }),
177 190 mergeMap((res) => {
178 191 if (res) {
179 192 return of(res);
... ... @@ -216,6 +229,7 @@ export class RuleChainPageComponent extends PageComponent
216 229 private ruleChainService: RuleChainService,
217 230 private authService: AuthService,
218 231 private translate: TranslateService,
  232 + private itembuffer: ItemBufferService,
219 233 public dialog: MatDialog,
220 234 public dialogService: DialogService,
221 235 public fb: FormBuilder) {
... ... @@ -236,6 +250,7 @@ export class RuleChainPageComponent extends PageComponent
236 250 })
237 251 )
238 252 .subscribe();
  253 + this.ruleChainCanvas.adjustCanvasSize(true);
239 254 }
240 255
241 256 onSearchTextUpdated(searchText: string) {
... ... @@ -244,6 +259,7 @@ export class RuleChainPageComponent extends PageComponent
244 259 }
245 260
246 261 private init() {
  262 + this.initHotKeys();
247 263 this.ruleChain = this.route.snapshot.data.ruleChain;
248 264 if (this.route.snapshot.data.import && !this.ruleChain) {
249 265 this.router.navigateByUrl('ruleChains');
... ... @@ -268,6 +284,89 @@ export class RuleChainPageComponent extends PageComponent
268 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 370 updateRuleChainLibrary() {
272 371 const search = this.ruleNodeTypeSearch.toUpperCase();
273 372 const res = this.ruleNodeComponents.filter(
... ... @@ -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 615 this.isDirtyValue = false;
514 616 this.updateRuleNodesHighlight();
515 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 835 onModelChanged() {
519 836 console.log('Model changed!');
520 837 this.isDirtyValue = true;
... ... @@ -531,6 +848,8 @@ export class RuleChainPageComponent extends PageComponent
531 848
532 849 openNodeDetails(node: FcRuleNode) {
533 850 if (node.component.type !== RuleNodeType.INPUT) {
  851 + this.enableHotKeys = false;
  852 + this.updateErrorTooltips(true);
534 853 this.isEditingRuleNodeLink = false;
535 854 this.editingRuleNodeLink = null;
536 855 this.isEditingRuleNode = true;
... ... @@ -545,6 +864,8 @@ export class RuleChainPageComponent extends PageComponent
545 864 openLinkDetails(edge: FcRuleEdge) {
546 865 const sourceNode: FcRuleNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source) as FcRuleNode;
547 866 if (sourceNode.component.type !== RuleNodeType.INPUT) {
  867 + this.enableHotKeys = false;
  868 + this.updateErrorTooltips(true);
548 869 this.isEditingRuleNode = false;
549 870 this.editingRuleNode = null;
550 871 this.editingRuleNodeLinkLabels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
... ... @@ -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 992 onDetailsDrawerClosed() {
562 993 this.onEditRuleNodeClosed();
563 994 this.onEditRuleNodeLinkClosed();
  995 + this.enableHotKeys = true;
  996 + this.updateErrorTooltips(false);
564 997 }
565 998
566 999 onEditRuleNodeClosed() {
... ... @@ -739,6 +1172,7 @@ export class RuleChainPageComponent extends PageComponent
739 1172 addRuleNode(ruleNode: FcRuleNode) {
740 1173 ruleNode.configuration = deepClone(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
741 1174 const ruleChainId = this.ruleChain.id ? this.ruleChain.id.id : null;
  1175 + this.enableHotKeys = false;
742 1176 this.dialog.open<AddRuleNodeDialogComponent, AddRuleNodeDialogData,
743 1177 FcRuleNode>(AddRuleNodeDialogComponent, {
744 1178 disableClose: true,
... ... @@ -772,6 +1206,7 @@ export class RuleChainPageComponent extends PageComponent
772 1206 this.onModelChanged();
773 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 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 1285 private displayTooltip(event: MouseEvent, content: string) {
840 1286 this.destroyTooltips();
841 1287 this.tooltipTimeout = setTimeout(() => {
... ...
... ... @@ -14,37 +14,32 @@
14 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 20 export interface FcRuleNodeTypeModel extends FcModel {
30 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 14 * limitations under the License.
15 15 */
16 16
  17 +@import './rule-node-colors';
  18 +
17 19 :host {
18 20
19 21 .fc-node-overlay {
... ... @@ -63,33 +65,7 @@
63 65 border: solid 1px #777;
64 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 70 &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) {
95 71 box-shadow: 0 0 10px 6px #51cbee;
... ...
... ... @@ -15,9 +15,9 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<hotkeys-cheatsheet></hotkeys-cheatsheet>
19 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 21 <mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px">
22 22 <mat-form-field floatLabel="always" hideRequiredMarker class="tb-widget-title">
23 23 <mat-label></mat-label>
... ...
... ... @@ -139,6 +139,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
139 139
140 140 saveWidgetTimeout: Timeout;
141 141
  142 + hotKeys: Hotkey[] = [];
  143 +
142 144 private rxSubscriptions = new Array<Subscription>();
143 145
144 146 constructor(protected store: Store<AppState>,
... ... @@ -146,7 +148,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
146 148 private route: ActivatedRoute,
147 149 private router: Router,
148 150 private widgetService: WidgetService,
149   - private hotkeysService: HotkeysService,
150 151 private translate: TranslateService,
151 152 private raf: RafService,
152 153 private dialog: MatDialog) {
... ... @@ -159,6 +160,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
159 160 this.init(data);
160 161 }
161 162 ));
  163 +
  164 + this.initHotKeys();
162 165 }
163 166
164 167 private init(data: any) {
... ... @@ -181,7 +184,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
181 184 }
182 185
183 186 ngOnInit(): void {
184   - this.initHotKeys();
185 187 this.initSplitLayout();
186 188 this.initAceEditors();
187 189 this.iframe = $(this.widgetIFrameElmRef.nativeElement);
... ... @@ -203,7 +205,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
203 205 }
204 206
205 207 private initHotKeys(): void {
206   - this.hotkeysService.add(
  208 + this.hotKeys.push(
207 209 new Hotkey('ctrl+q', (event: KeyboardEvent) => {
208 210 if (!getCurrentIsLoading(this.store) && !this.undoDisabled()) {
209 211 event.preventDefault();
... ... @@ -213,7 +215,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
213 215 }, ['INPUT', 'SELECT', 'TEXTAREA'],
214 216 this.translate.instant('widget.undo'))
215 217 );
216   - this.hotkeysService.add(
  218 + this.hotKeys.push(
217 219 new Hotkey('ctrl+s', (event: KeyboardEvent) => {
218 220 if (!getCurrentIsLoading(this.store) && !this.saveDisabled()) {
219 221 event.preventDefault();
... ... @@ -223,7 +225,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
223 225 }, ['INPUT', 'SELECT', 'TEXTAREA'],
224 226 this.translate.instant('widget.save'))
225 227 );
226   - this.hotkeysService.add(
  228 + this.hotKeys.push(
227 229 new Hotkey('shift+ctrl+s', (event: KeyboardEvent) => {
228 230 if (!getCurrentIsLoading(this.store) && !this.saveAsDisabled()) {
229 231 event.preventDefault();
... ... @@ -233,7 +235,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
233 235 }, ['INPUT', 'SELECT', 'TEXTAREA'],
234 236 this.translate.instant('widget.saveAs'))
235 237 );
236   - this.hotkeysService.add(
  238 + this.hotKeys.push(
237 239 new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => {
238 240 event.preventDefault();
239 241 this.fullscreen = !this.fullscreen;
... ... @@ -241,7 +243,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
241 243 }, ['INPUT', 'SELECT', 'TEXTAREA'],
242 244 this.translate.instant('widget.toggle-fullscreen'))
243 245 );
244   - this.hotkeysService.add(
  246 + this.hotKeys.push(
245 247 new Hotkey('ctrl+enter', (event: KeyboardEvent) => {
246 248 event.preventDefault();
247 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 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 22 import { Observable } from 'rxjs';
26 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 25 import { Store } from '@ngrx/store';
30 26 import { AppState } from '@core/core.state';
31 27 import { AbstractControl, FormGroup } from '@angular/forms';
... ... @@ -38,7 +34,6 @@ export enum MsgDataType {
38 34
39 35 export interface RuleNodeConfiguration {
40 36 [key: string]: any;
41   - // TODO:
42 37 }
43 38
44 39 export interface RuleNode extends BaseData<RuleNodeId> {
... ... @@ -307,6 +302,28 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor {
307 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 327 export interface TestScriptInputParams {
311 328 script: string;
312 329 scriptType: string;
... ...
... ... @@ -118,6 +118,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
118 118 import { MessageTypeAutocompleteComponent } from './components/message-type-autocomplete.component';
119 119 import { JsonContentComponent } from './components/json-content.component';
120 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 124 @NgModule({
123 125 providers: [
... ... @@ -149,11 +151,13 @@ import { KeyValMapComponent } from './components/kv-map.component';
149 151 FullscreenDirective,
150 152 CircularProgressDirective,
151 153 MatChipDraggableDirective,
  154 + TbHotkeysDirective,
152 155 TbAnchorComponent,
153 156 HelpComponent,
154 157 TbCheckboxComponent,
155 158 TbSnackBarComponent,
156 159 TbErrorComponent,
  160 + TbCheatSheetComponent,
157 161 BreadcrumbComponent,
158 162 UserMenuComponent,
159 163 TimewindowComponent,
... ... @@ -256,10 +260,12 @@ import { KeyValMapComponent } from './components/kv-map.component';
256 260 FullscreenDirective,
257 261 CircularProgressDirective,
258 262 MatChipDraggableDirective,
  263 + TbHotkeysDirective,
259 264 TbAnchorComponent,
260 265 HelpComponent,
261 266 TbCheckboxComponent,
262 267 TbErrorComponent,
  268 + TbCheatSheetComponent,
263 269 BreadcrumbComponent,
264 270 UserMenuComponent,
265 271 TimewindowComponent,
... ...