Browse Source

Rule chain page. Inprove hotkeys handling

pull/2411/head
Igor Kulikov 7 years ago
parent
commit
53b6aeb4fa
  1. 23
      msa/js-executor/package-lock.json
  2. 2
      ui-ngx/src/app/core/api/alias-controller.ts
  3. 120
      ui-ngx/src/app/core/services/item-buffer.service.ts
  4. 10
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  5. 3
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html
  6. 5
      ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html
  7. 29
      ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts
  8. 27
      ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.ts
  9. 46
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-colors.scss
  10. 4
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts
  11. 26
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts
  12. 39
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html
  13. 59
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss
  14. 480
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  15. 43
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.models.ts
  16. 30
      ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.scss
  17. 4
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  18. 16
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts
  19. 165
      ui-ngx/src/app/shared/components/cheatsheet.component.ts
  20. 86
      ui-ngx/src/app/shared/components/hotkeys.directive.ts
  21. 39
      ui-ngx/src/app/shared/models/rule-node.models.ts
  22. 6
      ui-ngx/src/app/shared/shared.module.ts

23
msa/js-executor/package-lock.json

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

2
ui-ngx/src/app/core/api/alias-controller.ts

@ -186,7 +186,9 @@ export class AliasController implements IAliasController {
);
} else {
resolvedAliasSubject.error(null);
const res = this.resolvedAliasesObservable[aliasId];
delete this.resolvedAliasesObservable[aliasId];
return res;
}
return this.resolvedAliasesObservable[aliasId];
}

120
ui-ngx/src/app/core/services/item-buffer.service.ts

@ -24,6 +24,8 @@ import * as equal from 'deep-equal';
import { UtilsService } from '@core/services/utils.service';
import { Observable, of, throwError } from 'rxjs';
import { map } from 'rxjs/operators';
import { FcRuleEdge, FcRuleNode, ruleNodeTypeDescriptors } from '@shared/models/rule-node.models';
import { RuleChainService } from '@core/http/rule-chain.service';
const WIDGET_ITEM = 'widget_item';
const WIDGET_REFERENCE = 'widget_reference';
@ -45,6 +47,21 @@ export interface WidgetReference {
originalColumns: number;
}
export interface RuleNodeConnection {
isInputSource: boolean;
fromIndex: number;
toIndex: number;
label: string;
labels: string[];
}
export interface RuleNodesReference {
nodes: FcRuleNode[];
connections: RuleNodeConnection[];
originX?: number;
originY?: number;
}
@Injectable({
providedIn: 'root'
})
@ -54,6 +71,7 @@ export class ItemBufferService {
private delimiter = '.';
constructor(private dashboardUtils: DashboardUtilsService,
private ruleChainService: RuleChainService,
private utils: UtilsService) {}
public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetItem {
@ -99,12 +117,12 @@ export class ItemBufferService {
public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void {
const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
this.storeSet(WIDGET_ITEM, JSON.stringify(widgetItem));
this.storeSet(WIDGET_ITEM, widgetItem);
}
public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void {
const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget);
this.storeSet(WIDGET_REFERENCE, JSON.stringify(widgetReference));
this.storeSet(WIDGET_REFERENCE, widgetReference);
}
public hasWidget(): boolean {
@ -112,9 +130,8 @@ export class ItemBufferService {
}
public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId): boolean {
const widgetReferenceJson = this.storeGet(WIDGET_REFERENCE);
if (widgetReferenceJson) {
const widgetReference: WidgetReference = JSON.parse(widgetReferenceJson);
const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE);
if (widgetReference) {
if (widgetReference.dashboardId === dashboard.id.id) {
if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout)
&& dashboard.configuration.widgets[widgetReference.widgetId]) {
@ -128,9 +145,8 @@ export class ItemBufferService {
public pasteWidget(targetDashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId, position: WidgetPosition,
onAliasesUpdateFunction: () => void): Observable<Widget> {
const widgetItemJson = this.storeGet(WIDGET_ITEM);
if (widgetItemJson) {
const widgetItem: WidgetItem = JSON.parse(widgetItemJson);
const widgetItem: WidgetItem = this.storeGet(WIDGET_ITEM);
if (widgetItem) {
const widget = widgetItem.widget;
const aliasesInfo = widgetItem.aliasesInfo;
const originalColumns = widgetItem.originalColumns;
@ -155,9 +171,8 @@ export class ItemBufferService {
public pasteWidgetReference(targetDashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId, position: WidgetPosition): Observable<Widget> {
const widgetReferenceJson = this.storeGet(WIDGET_REFERENCE);
if (widgetReferenceJson) {
const widgetReference: WidgetReference = JSON.parse(widgetReferenceJson);
const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE);
if (widgetReference) {
const widget = targetDashboard.configuration.widgets[widgetReference.widgetId];
if (widget) {
const originalColumns = widgetReference.originalColumns;
@ -216,6 +231,89 @@ export class ItemBufferService {
return of(theDashboard);
}
public copyRuleNodes(nodes: FcRuleNode[], connections: RuleNodeConnection[]) {
const ruleNodes: RuleNodesReference = {
nodes: [],
connections: []
};
let top = -1, left = -1, bottom = -1, right = -1;
for (let i = 0; i < nodes.length; i++) {
const origNode = nodes[i];
const node: FcRuleNode = {
id: '',
connectors: [],
additionalInfo: origNode.additionalInfo,
configuration: origNode.configuration,
debugMode: origNode.debugMode,
x: origNode.x,
y: origNode.y,
name: origNode.name,
componentClazz: origNode.component.clazz,
}
if (origNode.targetRuleChainId) {
node.targetRuleChainId = origNode.targetRuleChainId;
}
if (origNode.error) {
node.error = origNode.error;
}
ruleNodes.nodes.push(node);
if (i==0) {
top = node.y;
left = node.x;
bottom = node.y + 50;
right = node.x + 170;
} else {
top = Math.min(top, node.y);
left = Math.min(left, node.x);
bottom = Math.max(bottom, node.y + 50);
right = Math.max(right, node.x + 170);
}
}
ruleNodes.originX = left + (right-left)/2;
ruleNodes.originY = top + (bottom-top)/2;
connections.forEach(connection => {
ruleNodes.connections.push(connection);
});
this.storeSet(RULE_NODES, ruleNodes);
}
public hasRuleNodes(): boolean {
return this.storeHas(RULE_NODES);
}
public pasteRuleNodes(x: number, y: number): RuleNodesReference {
const ruleNodes: RuleNodesReference = this.storeGet(RULE_NODES);
if (ruleNodes) {
const deltaX = x - ruleNodes.originX;
const deltaY = y - ruleNodes.originY;
for (const node of ruleNodes.nodes) {
const component = this.ruleChainService.getRuleNodeComponentByClazz(node.componentClazz);
if (component) {
let icon = ruleNodeTypeDescriptors.get(component.type).icon;
let iconUrl: string = null;
if (component.configurationDescriptor.nodeDefinition.icon) {
icon = component.configurationDescriptor.nodeDefinition.icon;
}
if (component.configurationDescriptor.nodeDefinition.iconUrl) {
iconUrl = component.configurationDescriptor.nodeDefinition.iconUrl;
}
delete node.componentClazz;
node.component = component;
node.nodeClass = ruleNodeTypeDescriptors.get(component.type).nodeClass;
node.icon = icon;
node.iconUrl = iconUrl;
node.connectors = [];
node.x = Math.round(node.x + deltaX);
node.y = Math.round(node.y + deltaY);
} else {
return null;
}
}
return ruleNodes;
}
return null;
}
private getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId): number {
let originalColumns = 24;
let gridSettings = null;

10
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -737,18 +737,22 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
dataLoading: (subscription) => {
if (this.loadingData !== subscription.loadingData) {
this.loadingData = subscription.loadingData;
this.cd.detectChanges();
if (!this.destroyed) {
this.cd.detectChanges();
}
}
},
legendDataUpdated: (subscription, detectChanges) => {
if (detectChanges) {
if (detectChanges && !this.destroyed) {
this.cd.detectChanges();
}
},
timeWindowUpdated: (subscription, timeWindowConfig) => {
this.ngZone.run(() => {
this.widget.config.timewindow = timeWindowConfig;
this.cd.detectChanges();
if (!this.destroyed) {
this.cd.detectChanges();
}
});
}
};

3
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html

@ -17,6 +17,7 @@
-->
<div class="tb-dashboard-page mat-content" style="padding-top: 150px;"
fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
<tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
<section class="tb-dashboard-toolbar"
[ngClass]="{ 'tb-dashboard-toolbar-opened': toolbarOpened,
'tb-dashboard-toolbar-closed': !toolbarOpened }">
@ -146,6 +147,7 @@
[mode]="isMobile ? 'over' : 'side'"
[(opened)]="rightLayoutOpened">
<tb-dashboard-layout style="height: 100%;"
[dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="layouts.right.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
@ -159,6 +161,7 @@
[ngStyle]="{width: mainLayoutWidth(),
height: mainLayoutHeight()}">
<tb-dashboard-layout
[dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="layouts.main.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"

5
ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html

@ -15,13 +15,14 @@
limitations under the License.
-->
<hotkeys-cheatsheet></hotkeys-cheatsheet>
<div fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center"
*ngIf="layoutCtx.widgets.isLoading()">
<mat-spinner color="warn" mode="indeterminate" diameter="100">
</mat-spinner>
</div>
<div class="mat-content" style="position: relative; width: 100%; height: 100%;"
<div class="mat-content"
style="position: relative; width: 100%; height: 100%;" tb-hotkeys [hotkeys]="hotKeys"
[cheatSheet]="dashboardCheatSheet"
[style.backgroundImage]="backgroundImage"
[ngStyle]="dashboardStyle">
<section *ngIf="layoutCtx.widgets.isEmpty()" fxLayoutAlign="center center"

29
ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts

@ -14,27 +14,25 @@
/// limitations under the License.
///
import { Component, OnDestroy, OnInit, Input, ChangeDetectorRef, ViewChild } from '@angular/core';
import { StateControllerComponent } from '@home/pages/dashboard/states/state-controller.component';
import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ILayoutController } from '@home/pages/dashboard/layout/layout.models';
import { DashboardContext, DashboardPageLayoutContext } from '@home/pages/dashboard/dashboard-page.models';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Widget } from '@shared/models/widget.models';
import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
import { GridsterComponent } from 'angular-gridster2';
import {
DashboardCallbacks,
DashboardContextMenuItem,
IDashboardComponent, WidgetContextMenuItem
IDashboardComponent,
WidgetContextMenuItem
} from '@home/models/dashboard-component.models';
import { Observable, of, Subscription } from 'rxjs';
import { Subscription } from 'rxjs';
import { Hotkey, HotkeysService } from 'angular2-hotkeys';
import { getCurrentIsLoading } from '@core/interceptors/load.selectors';
import { TranslateService } from '@ngx-translate/core';
import { ItemBufferService } from '@app/core/services/item-buffer.service';
import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component';
@Component({
selector: 'tb-dashboard-layout',
@ -47,6 +45,10 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
dashboardStyle: {[klass: string]: any} = null;
backgroundImage: SafeStyle | string;
hotKeys: Hotkey[] = [];
@Input() dashboardCheatSheet: TbCheatSheetComponent;
@Input()
set layoutCtx(val: DashboardPageLayoutContext) {
this.layoutCtxValue = val;
@ -81,11 +83,11 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
private rxSubscriptions = new Array<Subscription>();
constructor(protected store: Store<AppState>,
private hotkeysService: HotkeysService,
private translate: TranslateService,
private itembuffer: ItemBufferService,
private sanitizer: DomSanitizer) {
super(store);
this.initHotKeys();
}
ngOnInit(): void {
@ -95,7 +97,6 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
this.dashboardCtx.runChangeDetection();
})
);
this.initHotKeys();
}
ngOnDestroy(): void {
@ -106,7 +107,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
}
private initHotKeys(): void {
this.hotkeysService.add(
this.hotKeys.push(
new Hotkey('ctrl+c', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
const widget = this.dashboard.getSelectedWidget();
@ -119,7 +120,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
}, null,
this.translate.instant('action.copy'))
);
this.hotkeysService.add(
this.hotKeys.push(
new Hotkey('ctrl+r', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
const widget = this.dashboard.getSelectedWidget();
@ -132,7 +133,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
}, null,
this.translate.instant('action.copy-reference'))
);
this.hotkeysService.add(
this.hotKeys.push(
new Hotkey('ctrl+v', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
if (this.itembuffer.hasWidget()) {
@ -144,7 +145,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
}, null,
this.translate.instant('action.paste'))
);
this.hotkeysService.add(
this.hotKeys.push(
new Hotkey('ctrl+i', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(),
@ -157,7 +158,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
}, null,
this.translate.instant('action.paste-reference'))
);
this.hotkeysService.add(
this.hotKeys.push(
new Hotkey('ctrl+x', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
const widget = this.dashboard.getSelectedWidget();

27
ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.ts

@ -14,31 +14,14 @@
/// limitations under the License.
///
import {
AfterViewInit,
Component, ElementRef,
EventEmitter, forwardRef,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, NgForm, Validators } from '@angular/forms';
import { FcRuleNode, FcRuleEdge } from './rulechain-page.models';
import { RuleNodeType, LinkLabel } from '@shared/models/rule-node.models';
import { EntityType } from '@shared/models/entity-type.models';
import { Observable, of, Subscription } from 'rxjs';
import { RuleChainService } from '@core/http/rule-chain.service';
import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { LinkLabel } from '@shared/models/rule-node.models';
import { Observable, of } from 'rxjs';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { deepClone } from '@core/utils';
import { EntityAlias } from '@shared/models/alias.models';
import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import { MatChipList, MatAutocomplete, MatChipInputEvent, MatAutocompleteSelectedEvent } from '@angular/material';
import { MatAutocomplete, MatAutocompleteSelectedEvent, MatChipInputEvent, MatChipList } from '@angular/material';
import { TranslateService } from '@ngx-translate/core';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { map, mergeMap, share, startWith } from 'rxjs/operators';

46
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-colors.scss

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@mixin rule-node-colors {
&.tb-filter-type {
background-color: #f1e861;
}
&.tb-enrichment-type {
background-color: #cdf14e;
}
&.tb-transformation-type {
background-color: #79cef1;
}
&.tb-action-type {
background-color: #f1928f;
}
&.tb-external-type {
background-color: #fbc766;
}
&.tb-rule-chain-type {
background-color: #d6c4f1;
}
&.tb-unknown-type {
background-color: #f16c29;
}
}

4
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts

@ -19,12 +19,10 @@ import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { FcRuleNode } from './rulechain-page.models';
import { RuleNodeType } from '@shared/models/rule-node.models';
import { FcRuleNode, RuleNodeType } from '@shared/models/rule-node.models';
import { EntityType } from '@shared/models/entity-type.models';
import { Subscription } from 'rxjs';
import { RuleChainService } from '@core/http/rule-chain.service';
import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component';
import { RuleNodeConfigComponent } from './rule-node-config.component';
@Component({

26
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts

@ -14,34 +14,12 @@
/// limitations under the License.
///
import {
AfterViewInit,
Component, ElementRef,
EventEmitter, forwardRef,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, NgForm, Validators } from '@angular/forms';
import { FcRuleNode, FcRuleEdge } from './rulechain-page.models';
import { RuleNodeType, LinkLabel } from '@shared/models/rule-node.models';
import { EntityType } from '@shared/models/entity-type.models';
import { Observable, of, Subscription } from 'rxjs';
import { RuleChainService } from '@core/http/rule-chain.service';
import { FcRuleEdge, LinkLabel } from '@shared/models/rule-node.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { deepClone } from '@core/utils';
import { EntityAlias } from '@shared/models/alias.models';
import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import { MatChipList, MatAutocomplete, MatChipInputEvent, MatAutocompleteSelectedEvent } from '@angular/material';
import { TranslateService } from '@ngx-translate/core';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { map, mergeMap, share } from 'rxjs/operators';
@Component({
selector: 'tb-rule-node-link',

39
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html

@ -15,8 +15,10 @@
limitations under the License.
-->
<div class="mat-content" fxFlex tb-fullscreen [fullscreen]="isFullscreen"
<div class="mat-content" fxFlex tb-fullscreen [fullscreen]="isFullscreen" tb-hotkeys [hotkeys]="hotKeys"
[cheatSheet]="cheatSheetComponent"
fxLayout="column" class="tb-rulechain">
<tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
<section class="tb-rulechain-container" fxFlex fxLayout="column">
<div class="tb-rulechain-layout" fxFlex fxLayout="row">
<section fxLayout="row"
@ -80,6 +82,7 @@
[model]="ruleNodeTypesModel[ruleNodeType].model"
[selectedObjects]="ruleNodeTypesModel[ruleNodeType].selectedObjects"
[automaticResize]="false"
fitModelSizeByDefault
[userCallbacks]="nodeLibCallbacks"
[nodeWidth]="170"
[nodeHeight]="50"
@ -162,7 +165,38 @@
matTooltipPosition="above">
<mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
<div class="tb-absolute-fill tb-rulechain-graph">
<div class="tb-absolute-fill tb-rulechain-graph" (contextmenu)="openRuleChainContextMenu($event)">
<div #ruleChainMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed"
[style.left]="ruleChainMenuPosition.x"
[style.top]="ruleChainMenuPosition.y"
[matMenuTriggerFor]="ruleChainMenu">
</div>
<mat-menu #ruleChainMenu="matMenu" class="tb-rule-chain-context-menu"
[overlapTrigger]="true">
<ng-template matMenuContent let-contextInfo="contextInfo">
<div class="tb-rule-chain-context-menu-container" (mouseleave)="onRuleChainContextMenuMouseLeave()">
<div class="tb-context-menu-header {{contextInfo.headerClass}}">
<mat-icon *ngIf="!contextInfo.iconUrl">{{contextInfo.icon}}</mat-icon>
<img *ngIf="contextInfo.iconUrl" [src]="contextInfo.iconUrl"/>
<div fxFlex>
<div class="tb-context-menu-title">{{contextInfo.title}}</div>
<div class="tb-context-menu-subtitle">{{contextInfo.subtitle}}</div>
</div>
</div>
<div *ngFor="let menuItem of contextInfo.menuItems">
<mat-divider *ngIf="menuItem.divider"></mat-divider>
<button *ngIf="!menuItem.divider"
mat-menu-item
[disabled]="!menuItem.enabled"
(click)="menuItem.action(contextMenuEvent)">
<span *ngIf="menuItem.shortcut" class="tb-alt-text"> {{ menuItem.shortcut | keyboardShortcut }}</span>
<mat-icon *ngIf="menuItem.icon">{{menuItem.icon}}</mat-icon>
<span translate>{{menuItem.value}}</span>
</button>
</div>
</div>
</ng-template>
</mat-menu>
<fc-canvas #ruleChainCanvas
id="tb-rulchain-canvas"
[model]="ruleChainModel"
@ -170,6 +204,7 @@
[selectedObjects]="selectedObjects"
[edgeStyle]="flowchartConstants.curvedStyle"
[automaticResize]="true"
fitModelSizeByDefault="false"
[nodeWidth]="170"
[nodeHeight]="50"
[dragAnimation]="flowchartConstants.dragAnimationRepaint"

59
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss

@ -14,6 +14,8 @@
* limitations under the License.
*/
@import './rule-node-colors';
.tb-rulechain {
width: 100%;
height: 100%;
@ -267,3 +269,60 @@
}
}
}
.tb-rule-chain-context-menu {
min-width: 256px;
max-height: 404px;
border-radius: 8px;
margin-left: -20px;
&.mat-menu-below {
margin-top: -60px;
}
.mat-menu-content {
padding: 0;
display: flex;
flex-direction: column;
.tb-rule-chain-context-menu-container {
pointer-events: auto;
padding: 0 0 8px;
display: flex;
flex-direction: column;
overflow-y: auto;
}
}
.tb-context-menu-header {
display: flex;
flex-direction: row;
height: 36px;
min-height: 36px;
padding: 8px 5px 5px;
font-size: 14px;
@include rule-node-colors();
&.tb-rulechain-header {
background-color: #aac7e4;
}
&.tb-link-header {
background-color: #aac7e4;
}
.mat-icon {
padding-right: 10px;
padding-left: 2px;
margin: auto;
}
.tb-context-menu-title {
font-weight: 500;
}
.tb-context-menu-subtitle {
font-size: 12px;
}
}
}

480
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts

@ -18,9 +18,11 @@ import {
AfterViewInit,
Component,
ElementRef,
HostBinding, Inject,
HostBinding,
Inject,
OnInit,
QueryList, SkipSelf,
QueryList,
SkipSelf,
ViewChild,
ViewChildren,
ViewEncapsulation
@ -31,7 +33,7 @@ import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog, MatExpansionPanel, ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialog, MatDialogRef, MatExpansionPanel } from '@angular/material';
import { DialogService } from '@core/services/dialog.service';
import { AuthService } from '@core/auth/auth.service';
import { ActivatedRoute, Router } from '@angular/router';
@ -41,8 +43,11 @@ import {
RuleChain,
ruleChainNodeComponent
} from '@shared/models/rule-chain.models';
import { FlowchartConstants, NgxFlowchartComponent, UserCallbacks } from 'ngx-flowchart/dist/ngx-flowchart';
import { FcItemInfo, FlowchartConstants, NgxFlowchartComponent, UserCallbacks } from 'ngx-flowchart/dist/ngx-flowchart';
import {
FcRuleEdge,
FcRuleNode,
FcRuleNodeType,
getRuleNodeHelpLink,
LinkLabel,
RuleNodeComponentDescriptor,
@ -50,24 +55,19 @@ import {
ruleNodeTypeDescriptors,
ruleNodeTypesLibrary
} from '@shared/models/rule-node.models';
import { FcRuleEdge, FcRuleNode, FcRuleNodeModel, FcRuleNodeType, FcRuleNodeTypeModel } from './rulechain-page.models';
import { FcRuleNodeModel, FcRuleNodeTypeModel, RuleChainMenuContextInfo } from './rulechain-page.models';
import { RuleChainService } from '@core/http/rule-chain.service';
import { fromEvent, never, of, throwError, NEVER, Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, tap, mergeMap } from 'rxjs/operators';
import { fromEvent, NEVER, Observable, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, mergeMap, tap } from 'rxjs/operators';
import { ISearchableComponent } from '../../models/searchable-component.models';
import { deepClone, isDefined, isString } from '@core/utils';
import { deepClone } from '@core/utils';
import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-details.component';
import { RuleNodeLinkComponent } from './rule-node-link.component';
import Timeout = NodeJS.Timeout;
import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { Widget, widgetTypesData } from '@shared/models/widget.models';
import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models';
import { DialogComponent } from '@shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { EntityService } from '@core/http/entity.service';
import { AddWidgetDialogComponent, AddWidgetDialogData } from '@home/pages/dashboard/add-widget-dialog.component';
import { RuleNodeConfigComponent } from '@home/pages/rulechain/rule-node-config.component';
import { MatMenuTrigger } from '@angular/material/menu';
import { ItemBufferService, RuleNodeConnection } from '@core/services/item-buffer.service';
import Timeout = NodeJS.Timeout;
import { Hotkey, HotkeysService } from 'angular2-hotkeys';
@Component({
selector: 'tb-rulechain-page',
@ -92,6 +92,12 @@ export class RuleChainPageComponent extends PageComponent
@ViewChildren('ruleNodeTypeExpansionPanels',
{read: MatExpansionPanel}) expansionPanels: QueryList<MatExpansionPanel>;
@ViewChild('ruleChainMenuTrigger', {static: true}) ruleChainMenuTrigger: MatMenuTrigger;
ruleChainMenuPosition = { x: '0px', y: '0px' };
contextMenuEvent: MouseEvent;
ruleNodeTypeDescriptorsMap = ruleNodeTypeDescriptors;
ruleNodeTypesLibraryArray = ruleNodeTypesLibrary;
@ -116,6 +122,9 @@ export class RuleChainPageComponent extends PageComponent
isEditingRuleNodeLink = false;
editingRuleNodeLinkIndex = -1;
hotKeys: Hotkey[] = [];
enableHotKeys = true;
isLibraryOpen = true;
ruleNodeSearch = '';
@ -173,7 +182,11 @@ export class RuleChainPageComponent extends PageComponent
} else {
const labels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
const allowCustomLabels = this.ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component);
this.enableHotKeys = false;
return this.addRuleNodeLink(edge, labels, allowCustomLabels).pipe(
tap(() => {
this.enableHotKeys = true;
}),
mergeMap((res) => {
if (res) {
return of(res);
@ -216,6 +229,7 @@ export class RuleChainPageComponent extends PageComponent
private ruleChainService: RuleChainService,
private authService: AuthService,
private translate: TranslateService,
private itembuffer: ItemBufferService,
public dialog: MatDialog,
public dialogService: DialogService,
public fb: FormBuilder) {
@ -236,6 +250,7 @@ export class RuleChainPageComponent extends PageComponent
})
)
.subscribe();
this.ruleChainCanvas.adjustCanvasSize(true);
}
onSearchTextUpdated(searchText: string) {
@ -244,6 +259,7 @@ export class RuleChainPageComponent extends PageComponent
}
private init() {
this.initHotKeys();
this.ruleChain = this.route.snapshot.data.ruleChain;
if (this.route.snapshot.data.import && !this.ruleChain) {
this.router.navigateByUrl('ruleChains');
@ -268,6 +284,89 @@ export class RuleChainPageComponent extends PageComponent
this.createRuleChainModel();
}
private initHotKeys(): void {
this.hotKeys.push(
new Hotkey('ctrl+a', (event: KeyboardEvent) => {
if (this.enableHotKeys) {
event.preventDefault();
this.ruleChainCanvas.modelService.selectAll();
return false;
}
return true;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('rulenode.select-all-objects'))
);
this.hotKeys.push(
new Hotkey('ctrl+c', (event: KeyboardEvent) => {
if (this.enableHotKeys) {
event.preventDefault();
this.copyRuleNodes();
return false;
}
return true;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('rulenode.copy-selected'))
);
this.hotKeys.push(
new Hotkey('ctrl+v', (event: KeyboardEvent) => {
if (this.enableHotKeys) {
event.preventDefault();
if (this.itembuffer.hasRuleNodes()) {
this.pasteRuleNodes();
}
return false;
}
return true;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('action.paste'))
);
this.hotKeys.push(
new Hotkey('esc', (event: KeyboardEvent) => {
if (this.enableHotKeys) {
event.preventDefault();
event.stopPropagation();
this.ruleChainCanvas.modelService.deselectAll();
return false;
}
return true;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('rulenode.deselect-all-objects'))
);
this.hotKeys.push(
new Hotkey('ctrl+s', (event: KeyboardEvent) => {
if (this.enableHotKeys) {
event.preventDefault();
this.saveRuleChain();
return false;
}
return true;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('action.apply'))
);
this.hotKeys.push(
new Hotkey('ctrl+z', (event: KeyboardEvent) => {
if (this.enableHotKeys) {
event.preventDefault();
this.revertRuleChain();
return false;
}
return true;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('action.decline-changes'))
);
this.hotKeys.push(
new Hotkey('del', (event: KeyboardEvent) => {
if (this.enableHotKeys) {
event.preventDefault();
this.ruleChainCanvas.modelService.deleteSelected();
return false;
}
return true;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('rulenode.delete-selected-objects'))
);
}
updateRuleChainLibrary() {
const search = this.ruleNodeTypeSearch.toUpperCase();
const res = this.ruleNodeComponents.filter(
@ -510,11 +609,229 @@ export class RuleChainPageComponent extends PageComponent
}
});
}
if (this.ruleChainCanvas) {
this.ruleChainCanvas.adjustCanvasSize(true);
}
this.isDirtyValue = false;
this.updateRuleNodesHighlight();
this.validate();
}
openRuleChainContextMenu($event: MouseEvent) {
if (this.ruleChainCanvas.modelService && !$event.ctrlKey && !$event.metaKey) {
const x = $event.clientX;
const y = $event.clientY;
const item = this.ruleChainCanvas.modelService.getItemInfoAtPoint(x, y);
const contextInfo = this.prepareContextMenu(item);
if (contextInfo.menuItems && contextInfo.menuItems.length > 0) {
$event.preventDefault();
$event.stopPropagation();
this.contextMenuEvent = $event;
this.ruleChainMenuPosition.x = x + 'px';
this.ruleChainMenuPosition.y = y + 'px';
this.ruleChainMenuTrigger.menuData = { contextInfo };
this.ruleChainMenuTrigger.openMenu();
}
}
}
onRuleChainContextMenuMouseLeave() {
this.ruleChainMenuTrigger.closeMenu();
}
private prepareContextMenu(item: FcItemInfo): RuleChainMenuContextInfo {
if (this.objectsSelected() || (!item.node && !item.edge)) {
return this.prepareRuleChainContextMenu();
} else if (item.node) {
return this.prepareRuleNodeContextMenu(item.node);
} else if (item.edge) {
return this.prepareEdgeContextMenu(item.edge);
}
}
private prepareRuleChainContextMenu(): RuleChainMenuContextInfo {
const contextInfo: RuleChainMenuContextInfo = {
headerClass: 'tb-rulechain-header',
icon: 'settings_ethernet',
title: this.ruleChain.name,
subtitle: this.translate.instant('rulechain.rulechain'),
menuItems: []
};
if (this.ruleChainCanvas.modelService.nodes.getSelectedNodes().length) {
contextInfo.menuItems.push(
{
action: () => {
this.copyRuleNodes();
},
enabled: true,
value: 'rulenode.copy-selected',
icon: 'content_copy',
shortcut: 'M-C'
}
);
}
contextInfo.menuItems.push(
{
action: ($event) => {
this.pasteRuleNodes($event);
},
enabled: this.itembuffer.hasRuleNodes(),
value: 'action.paste',
icon: 'content_paste',
shortcut: 'M-V'
}
);
contextInfo.menuItems.push(
{
divider: true
}
);
if (this.objectsSelected()) {
contextInfo.menuItems.push(
{
action: () => {
this.ruleChainCanvas.modelService.deselectAll();
},
enabled: true,
value: 'rulenode.deselect-all',
icon: 'tab_unselected',
shortcut: 'Esc'
}
);
contextInfo.menuItems.push(
{
action: () => {
this.ruleChainCanvas.modelService.deleteSelected();
},
enabled: true,
value: 'rulenode.delete-selected',
icon: 'clear',
shortcut: 'Del'
}
);
} else {
contextInfo.menuItems.push(
{
action: () => {
this.ruleChainCanvas.modelService.selectAll();
},
enabled: true,
value: 'rulenode.select-all',
icon: 'select_all',
shortcut: 'M-A'
}
);
}
contextInfo.menuItems.push(
{
divider: true
}
);
contextInfo.menuItems.push(
{
action: () => {
this.saveRuleChain();
},
enabled: !(this.isInvalid || (!this.isDirty && !this.isImport)),
value: 'action.apply-changes',
icon: 'done',
shortcut: 'M-S'
}
);
contextInfo.menuItems.push(
{
action: () => {
this.revertRuleChain();
},
enabled: this.isDirty,
value: 'action.decline-changes',
icon: 'close',
shortcut: 'M-Z'
}
);
return contextInfo;
}
private prepareRuleNodeContextMenu(node: FcRuleNode): RuleChainMenuContextInfo {
const contextInfo: RuleChainMenuContextInfo = {
headerClass: node.nodeClass,
icon: node.icon,
iconUrl: node.iconUrl,
title: node.name,
subtitle: node.component.name,
menuItems: []
};
if (!node.readonly) {
contextInfo.menuItems.push(
{
action: () => {
this.openNodeDetails(node);
},
enabled: true,
value: 'rulenode.details',
icon: 'menu'
}
);
contextInfo.menuItems.push(
{
action: () => {
this.copyNode(node);
},
enabled: true,
value: 'action.copy',
icon: 'content_copy'
}
);
contextInfo.menuItems.push(
{
action: () => {
this.ruleChainCanvas.modelService.nodes.delete(node);
},
enabled: true,
value: 'action.delete',
icon: 'clear',
shortcut: 'M-X'
}
);
}
return contextInfo;
}
private prepareEdgeContextMenu(edge: FcRuleEdge): RuleChainMenuContextInfo {
const contextInfo: RuleChainMenuContextInfo = {
headerClass: 'tb-link-header',
icon: 'trending_flat',
title: edge.label,
subtitle: this.translate.instant('rulenode.link'),
menuItems: []
};
const sourceNode: FcRuleNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source);
if (sourceNode.component.type != RuleNodeType.INPUT) {
contextInfo.menuItems.push(
{
action: () => {
this.openLinkDetails(edge);
},
enabled: true,
value: 'rulenode.details',
icon: 'menu'
}
);
}
contextInfo.menuItems.push(
{
action: () => {
this.ruleChainCanvas.modelService.edges.delete(edge);
},
enabled: true,
value: 'action.delete',
icon: 'clear',
shortcut: 'M-X'
}
);
return contextInfo;
}
onModelChanged() {
console.log('Model changed!');
this.isDirtyValue = true;
@ -531,6 +848,8 @@ export class RuleChainPageComponent extends PageComponent
openNodeDetails(node: FcRuleNode) {
if (node.component.type !== RuleNodeType.INPUT) {
this.enableHotKeys = false;
this.updateErrorTooltips(true);
this.isEditingRuleNodeLink = false;
this.editingRuleNodeLink = null;
this.isEditingRuleNode = true;
@ -545,6 +864,8 @@ export class RuleChainPageComponent extends PageComponent
openLinkDetails(edge: FcRuleEdge) {
const sourceNode: FcRuleNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source) as FcRuleNode;
if (sourceNode.component.type !== RuleNodeType.INPUT) {
this.enableHotKeys = false;
this.updateErrorTooltips(true);
this.isEditingRuleNode = false;
this.editingRuleNode = null;
this.editingRuleNodeLinkLabels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
@ -558,9 +879,121 @@ export class RuleChainPageComponent extends PageComponent
}
}
private copyNode(node: FcRuleNode) {
this.itembuffer.copyRuleNodes([node], []);
}
private copyRuleNodes() {
const nodes: FcRuleNode[] = this.ruleChainCanvas.modelService.nodes.getSelectedNodes();
const edges: FcRuleEdge[] = this.ruleChainCanvas.modelService.edges.getSelectedEdges();
const connections: RuleNodeConnection[] = [];
edges.forEach((edge) => {
const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source);
const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination);
const isInputSource = sourceNode.component.type == RuleNodeType.INPUT;
const fromIndex = nodes.indexOf(sourceNode);
const toIndex = nodes.indexOf(destNode);
if ( (isInputSource || fromIndex > -1) && toIndex > -1 ) {
const connection: RuleNodeConnection = {
isInputSource: isInputSource,
fromIndex: fromIndex,
toIndex: toIndex,
label: edge.label,
labels: edge.labels
};
connections.push(connection);
}
});
this.itembuffer.copyRuleNodes(nodes, connections);
}
private pasteRuleNodes(event?: MouseEvent) {
const canvas = $(this.ruleChainCanvas.modelService.canvasHtmlElement);
let x: number;
let y: number;
if (event) {
const offset = canvas.offset();
x = Math.round(event.clientX - offset.left);
y = Math.round(event.clientY - offset.top);
} else {
const scrollParent = canvas.parent();
const scrollTop = scrollParent.scrollTop();
const scrollLeft = scrollParent.scrollLeft();
x = scrollLeft + scrollParent.width()/2;
y = scrollTop + scrollParent.height()/2;
}
const ruleNodes = this.itembuffer.pasteRuleNodes(x, y);
if (ruleNodes) {
this.ruleChainCanvas.modelService.deselectAll();
const nodes: FcRuleNode[] = [];
ruleNodes.nodes.forEach((node) => {
node.id = 'rule-chain-node-' + this.nextNodeID++;
const component = node.component;
if (component.configurationDescriptor.nodeDefinition.inEnabled) {
node.connectors.push(
{
type: FlowchartConstants.leftConnectorType,
id: (this.nextConnectorID++) + ''
}
);
}
if (component.configurationDescriptor.nodeDefinition.outEnabled) {
node.connectors.push(
{
type: FlowchartConstants.rightConnectorType,
id: (this.nextConnectorID++) + ''
}
);
}
nodes.push(node);
this.ruleChainModel.nodes.push(node);
this.ruleChainCanvas.modelService.nodes.select(node);
});
ruleNodes.connections.forEach((connection) => {
const sourceNode = nodes[connection.fromIndex];
const destNode = nodes[connection.toIndex];
if ( (connection.isInputSource || sourceNode) && destNode ) {
let source: string;
let destination: string;
if (connection.isInputSource) {
source = this.inputConnectorId + '';
const found = this.ruleChainModel.edges.find(theEdge => theEdge.source === (this.inputConnectorId + ''));
if (found) {
this.ruleChainCanvas.modelService.edges.delete(found);
}
} else {
const sourceConnectors = this.ruleChainCanvas.modelService.nodes.getConnectorsByType(sourceNode, FlowchartConstants.rightConnectorType);
if (sourceConnectors && sourceConnectors.length) {
source = sourceConnectors[0].id;
}
}
const destConnectors = this.ruleChainCanvas.modelService.nodes.getConnectorsByType(destNode, FlowchartConstants.leftConnectorType);
if (destConnectors && destConnectors.length) {
destination = destConnectors[0].id;
}
if (source && destination) {
const edge: FcRuleEdge = {
source: source,
destination: destination,
label: connection.label,
labels: connection.labels
};
this.ruleChainModel.edges.push(edge);
this.ruleChainCanvas.modelService.edges.select(edge);
}
}
});
this.updateRuleNodesHighlight();
this.validate();
this.onModelChanged();
}
}
onDetailsDrawerClosed() {
this.onEditRuleNodeClosed();
this.onEditRuleNodeLinkClosed();
this.enableHotKeys = true;
this.updateErrorTooltips(false);
}
onEditRuleNodeClosed() {
@ -739,6 +1172,7 @@ export class RuleChainPageComponent extends PageComponent
addRuleNode(ruleNode: FcRuleNode) {
ruleNode.configuration = deepClone(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
const ruleChainId = this.ruleChain.id ? this.ruleChain.id.id : null;
this.enableHotKeys = false;
this.dialog.open<AddRuleNodeDialogComponent, AddRuleNodeDialogData,
FcRuleNode>(AddRuleNodeDialogComponent, {
disableClose: true,
@ -772,6 +1206,7 @@ export class RuleChainPageComponent extends PageComponent
this.onModelChanged();
this.updateRuleNodesHighlight();
}
this.enableHotKeys = true;
}
);
}
@ -836,6 +1271,17 @@ export class RuleChainPageComponent extends PageComponent
}
}
private updateErrorTooltips(hide: boolean) {
for (const nodeId of Object.keys(this.errorTooltips)) {
const tooltip = this.errorTooltips[nodeId];
if (hide) {
tooltip.close();
} else {
tooltip.open();
}
}
}
private displayTooltip(event: MouseEvent, content: string) {
this.destroyTooltips();
this.tooltipTimeout = setTimeout(() => {

43
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.models.ts

@ -14,37 +14,32 @@
/// limitations under the License.
///
import { FcNode, FcEdge, FcModel } from 'ngx-flowchart/dist/ngx-flowchart';
import { RuleNodeComponentDescriptor, RuleNodeConfiguration } from '@shared/models/rule-node.models';
import { RuleNodeId } from '@app/shared/models/id/rule-node-id';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
export interface FcRuleNodeType extends FcNode {
component: RuleNodeComponentDescriptor;
nodeClass: string;
icon: string;
iconUrl?: string;
}
import { FcModel } from 'ngx-flowchart/dist/ngx-flowchart';
import { FcRuleEdge, FcRuleNode, FcRuleNodeType } from '@shared/models/rule-node.models';
export interface FcRuleNodeTypeModel extends FcModel {
nodes: Array<FcRuleNodeType>;
}
export interface FcRuleNode extends FcRuleNodeType {
ruleNodeId?: RuleNodeId;
additionalInfo?: any;
configuration?: RuleNodeConfiguration;
debugMode?: boolean;
targetRuleChainId?: string;
error?: string;
highlighted?: boolean;
export interface FcRuleNodeModel extends FcModel {
nodes: Array<FcRuleNode>;
edges: Array<FcRuleEdge>;
}
export interface FcRuleEdge extends FcEdge {
labels?: string[];
export interface RuleChainMenuItem {
action?: ($event: MouseEvent) => void;
enabled?: boolean;
value?: string;
icon?: string;
shortcut?: string;
divider?: boolean;
}
export interface FcRuleNodeModel extends FcModel {
nodes: Array<FcRuleNode>;
edges: Array<FcRuleEdge>;
export interface RuleChainMenuContextInfo {
headerClass: string;
icon: string;
iconUrl?: string;
title: string;
subtitle: string;
menuItems: RuleChainMenuItem[];
}

30
ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.scss

@ -14,6 +14,8 @@
* limitations under the License.
*/
@import './rule-node-colors';
:host {
.fc-node-overlay {
@ -63,33 +65,7 @@
border: solid 1px #777;
border-radius: 5px;
&.tb-filter-type {
background-color: #f1e861;
}
&.tb-enrichment-type {
background-color: #cdf14e;
}
&.tb-transformation-type {
background-color: #79cef1;
}
&.tb-action-type {
background-color: #f1928f;
}
&.tb-external-type {
background-color: #fbc766;
}
&.tb-rule-chain-type {
background-color: #d6c4f1;
}
&.tb-unknown-type {
background-color: #f16c29;
}
@include rule-node-colors();
&.tb-rule-node-highlighted:not(.tb-rule-node-invalid) {
box-shadow: 0 0 10px 6px #51cbee;

4
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html

@ -15,9 +15,9 @@
limitations under the License.
-->
<hotkeys-cheatsheet></hotkeys-cheatsheet>
<div fxFlex fxLayout="column">
<div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen">
<div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen" tb-hotkeys [hotkeys]="hotKeys" [cheatSheet]="cheatSheetComponent">
<tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
<mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px">
<mat-form-field floatLabel="always" hideRequiredMarker class="tb-widget-title">
<mat-label></mat-label>

16
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts

@ -139,6 +139,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
saveWidgetTimeout: Timeout;
hotKeys: Hotkey[] = [];
private rxSubscriptions = new Array<Subscription>();
constructor(protected store: Store<AppState>,
@ -146,7 +148,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
private route: ActivatedRoute,
private router: Router,
private widgetService: WidgetService,
private hotkeysService: HotkeysService,
private translate: TranslateService,
private raf: RafService,
private dialog: MatDialog) {
@ -159,6 +160,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
this.init(data);
}
));
this.initHotKeys();
}
private init(data: any) {
@ -181,7 +184,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
}
ngOnInit(): void {
this.initHotKeys();
this.initSplitLayout();
this.initAceEditors();
this.iframe = $(this.widgetIFrameElmRef.nativeElement);
@ -203,7 +205,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
}
private initHotKeys(): void {
this.hotkeysService.add(
this.hotKeys.push(
new Hotkey('ctrl+q', (event: KeyboardEvent) => {
if (!getCurrentIsLoading(this.store) && !this.undoDisabled()) {
event.preventDefault();
@ -213,7 +215,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('widget.undo'))
);
this.hotkeysService.add(
this.hotKeys.push(
new Hotkey('ctrl+s', (event: KeyboardEvent) => {
if (!getCurrentIsLoading(this.store) && !this.saveDisabled()) {
event.preventDefault();
@ -223,7 +225,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('widget.save'))
);
this.hotkeysService.add(
this.hotKeys.push(
new Hotkey('shift+ctrl+s', (event: KeyboardEvent) => {
if (!getCurrentIsLoading(this.store) && !this.saveAsDisabled()) {
event.preventDefault();
@ -233,7 +235,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('widget.saveAs'))
);
this.hotkeysService.add(
this.hotKeys.push(
new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => {
event.preventDefault();
this.fullscreen = !this.fullscreen;
@ -241,7 +243,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('widget.toggle-fullscreen'))
);
this.hotkeysService.add(
this.hotKeys.push(
new Hotkey('ctrl+enter', (event: KeyboardEvent) => {
event.preventDefault();
this.applyWidgetScript();

165
ui-ngx/src/app/shared/components/cheatsheet.component.ts

@ -0,0 +1,165 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
import { Hotkey, HotkeysService } from 'angular2-hotkeys';
@Component({
selector : 'tb-hotkeys-cheatsheet',
styles : [`
.tb-hotkeys-container {
display: table !important;
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
color: #333;
font-size: 1em;
background-color: rgba(255,255,255,0.9);
outline: 0;
}
.tb-hotkeys-container.fade {
z-index: -1024;
visibility: hidden;
opacity: 0;
-webkit-transition: opacity 0.15s linear;
-moz-transition: opacity 0.15s linear;
-o-transition: opacity 0.15s linear;
transition: opacity 0.15s linear;
}
.tb-hotkeys-container.fade.in {
z-index: 10002;
visibility: visible;
opacity: 1;
}
.tb-hotkeys-title {
font-weight: bold;
text-align: center;
font-size: 1.2em;
}
.tb-hotkeys {
width: 100%;
height: 100%;
display: table-cell;
vertical-align: middle;
}
.tb-hotkeys table {
margin: auto;
color: #333;
}
.tb-content {
display: table-cell;
vertical-align: middle;
}
.tb-hotkeys-keys {
padding: 5px;
text-align: right;
}
.tb-hotkeys-key {
display: inline-block;
color: #fff;
background-color: #333;
border: 1px solid #333;
border-radius: 5px;
text-align: center;
margin-right: 5px;
box-shadow: inset 0 1px 0 #666, 0 1px 0 #bbb;
padding: 5px 9px;
font-size: 1em;
}
.tb-hotkeys-text {
padding-left: 10px;
font-size: 1em;
}
.tb-hotkeys-close {
position: fixed;
top: 20px;
right: 20px;
font-size: 2em;
font-weight: bold;
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 5px;
min-height: 45px;
min-width: 45px;
text-align: center;
}
.tb-hotkeys-close:hover {
background-color: #fff;
cursor: pointer;
}
@media all and (max-width: 500px) {
.tb-hotkeys {
font-size: 0.8em;
}
}
@media all and (min-width: 750px) {
.tb-hotkeys {
font-size: 1.2em;
}
} `],
template : `<div tabindex="-1" class="tb-hotkeys-container fade" [ngClass]="{'in': helpVisible}" style="display:none"><div class="tb-hotkeys">
<h4 class="tb-hotkeys-title">{{ title }}</h4>
<table *ngIf="helpVisible"><tbody>
<tr *ngFor="let hotkey of hotkeysList">
<td class="tb-hotkeys-keys">
<span *ngFor="let key of hotkey.formatted" class="tb-hotkeys-key">{{ key }}</span>
</td>
<td class="tb-hotkeys-text">{{ hotkey.description }}</td>
</tr>
</tbody></table>
<div class="tb-hotkeys-close" (click)="toggleCheatSheet()">&#215;</div>
</div></div>`,
})
export class TbCheatSheetComponent implements OnInit, OnDestroy {
helpVisible = false;
@Input() title: string = 'Keyboard Shortcuts:';
@Input()
hotkeys: Hotkey[];
hotkeysList: Hotkey[];
private mousetrap: MousetrapInstance;
constructor(private _elementRef: ElementRef,
private hotkeysService: HotkeysService) {
this.mousetrap = new Mousetrap(this._elementRef.nativeElement);
this.mousetrap.bind('?', (event: KeyboardEvent, combo: string) => {
this.toggleCheatSheet();
});
}
public ngOnInit(): void {
if (this.hotkeys) {
this.hotkeysList = this.hotkeys.filter(hotkey => hotkey.description);
}
}
public setHotKeys(hotkeys: Hotkey[]) {
this.hotkeysList = hotkeys.filter(hotkey => hotkey.description);
}
public toggleCheatSheet(): void {
this.helpVisible = !this.helpVisible;
}
ngOnDestroy() {
this.mousetrap.unbind('?');
}
}

86
ui-ngx/src/app/shared/components/hotkeys.directive.ts

@ -0,0 +1,86 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {Directive, Input, OnInit, OnDestroy, ElementRef} from '@angular/core';
import {Hotkey, ExtendedKeyboardEvent} from 'angular2-hotkeys';
import 'mousetrap';
import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component';
@Directive({
selector : '[tb-hotkeys]'
})
export class TbHotkeysDirective implements OnInit, OnDestroy {
@Input() hotkeys: Hotkey[] = [];
@Input() cheatSheet: TbCheatSheetComponent;
private mousetrap: MousetrapInstance;
private hotkeysList: Hotkey[] = [];
private _preventIn = ['INPUT', 'SELECT', 'TEXTAREA'];
constructor(private _elementRef: ElementRef) {
this.mousetrap = new Mousetrap(this._elementRef.nativeElement);
(this._elementRef.nativeElement as HTMLElement).tabIndex = -1;
(this._elementRef.nativeElement as HTMLElement).style.outline = '0';
}
ngOnInit() {
for (let hotkey of this.hotkeys) {
this.hotkeysList.push(hotkey);
this.bindEvent(hotkey);
}
if (this.cheatSheet) {
let hotkeyObj: Hotkey = new Hotkey(
'?',
(event: KeyboardEvent) => {
this.cheatSheet.toggleCheatSheet();
return false;
},
[],
'Show / hide this help menu',
);
this.hotkeysList.unshift(hotkeyObj);
this.bindEvent(hotkeyObj);
this.cheatSheet.setHotKeys(this.hotkeysList);
}
}
private bindEvent(hotkey: Hotkey): void {
this.mousetrap.bind((<Hotkey>hotkey).combo, (event: KeyboardEvent, combo: string) => {
let shouldExecute = true;
if(event) {
let target: HTMLElement = <HTMLElement>(event.target || event.srcElement);
let nodeName: string = target.nodeName.toUpperCase();
if((' ' + target.className + ' ').indexOf(' mousetrap ') > -1) {
shouldExecute = true;
} else if(this._preventIn.indexOf(nodeName) > -1 && (<Hotkey>hotkey).allowIn.map(allow => allow.toUpperCase()).indexOf(nodeName) === -1) {
shouldExecute = false;
}
}
if(shouldExecute) {
return (<Hotkey>hotkey).callback.apply(this, [event, combo]);
}
});
}
ngOnDestroy() {
for (let hotkey of this.hotkeysList) {
this.mousetrap.unbind(hotkey.combo);
}
}
}

39
ui-ngx/src/app/shared/models/rule-node.models.ts

@ -14,18 +14,14 @@
/// limitations under the License.
///
import {BaseData} from '@shared/models/base-data';
import {AssetId} from '@shared/models/id/asset-id';
import {TenantId} from '@shared/models/id/tenant-id';
import {CustomerId} from '@shared/models/id/customer-id';
import {RuleChainId} from '@shared/models/id/rule-chain-id';
import {RuleNodeId} from '@shared/models/id/rule-node-id';
import { ComponentDescriptor, ComponentType } from '@shared/models/component-descriptor.models';
import { EntityType, EntityTypeResource } from '@shared/models/entity-type.models';
import { BaseData } from '@shared/models/base-data';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { RuleNodeId } from '@shared/models/id/rule-node-id';
import { ComponentDescriptor } from '@shared/models/component-descriptor.models';
import { FcEdge, FcNode } from 'ngx-flowchart/dist/ngx-flowchart';
import { Observable } from 'rxjs';
import { PageComponent } from '@shared/components/page.component';
import { AfterViewInit, ComponentFactory, EventEmitter, Inject, OnDestroy, OnInit } from '@angular/core';
import { RafService } from '@core/services/raf.service';
import { AfterViewInit, EventEmitter, Inject, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, FormGroup } from '@angular/forms';
@ -38,7 +34,6 @@ export enum MsgDataType {
export interface RuleNodeConfiguration {
[key: string]: any;
// TODO:
}
export interface RuleNode extends BaseData<RuleNodeId> {
@ -307,6 +302,28 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor {
configurationDescriptor?: RuleNodeConfigurationDescriptor;
}
export interface FcRuleNodeType extends FcNode {
component?: RuleNodeComponentDescriptor;
nodeClass?: string;
icon?: string;
iconUrl?: string;
}
export interface FcRuleNode extends FcRuleNodeType {
ruleNodeId?: RuleNodeId;
additionalInfo?: any;
configuration?: RuleNodeConfiguration;
debugMode?: boolean;
targetRuleChainId?: string;
error?: string;
highlighted?: boolean;
componentClazz?: string;
}
export interface FcRuleEdge extends FcEdge {
labels?: string[];
}
export interface TestScriptInputParams {
script: string;
scriptType: string;

6
ui-ngx/src/app/shared/shared.module.ts

@ -118,6 +118,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
import { MessageTypeAutocompleteComponent } from './components/message-type-autocomplete.component';
import { JsonContentComponent } from './components/json-content.component';
import { KeyValMapComponent } from './components/kv-map.component';
import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component';
import { TbHotkeysDirective } from '@shared/components/hotkeys.directive';
@NgModule({
providers: [
@ -149,11 +151,13 @@ import { KeyValMapComponent } from './components/kv-map.component';
FullscreenDirective,
CircularProgressDirective,
MatChipDraggableDirective,
TbHotkeysDirective,
TbAnchorComponent,
HelpComponent,
TbCheckboxComponent,
TbSnackBarComponent,
TbErrorComponent,
TbCheatSheetComponent,
BreadcrumbComponent,
UserMenuComponent,
TimewindowComponent,
@ -256,10 +260,12 @@ import { KeyValMapComponent } from './components/kv-map.component';
FullscreenDirective,
CircularProgressDirective,
MatChipDraggableDirective,
TbHotkeysDirective,
TbAnchorComponent,
HelpComponent,
TbCheckboxComponent,
TbErrorComponent,
TbCheatSheetComponent,
BreadcrumbComponent,
UserMenuComponent,
TimewindowComponent,

Loading…
Cancel
Save