Browse Source

RuleChain UI

pull/2411/head
Igor Kulikov 7 years ago
parent
commit
70e81e84eb
  1. 23
      msa/js-executor/package-lock.json
  2. 1
      ui-ngx/angular.json
  3. 18
      ui-ngx/src/app/core/http/rule-chain.service.ts
  4. 6
      ui-ngx/src/app/modules/home/components/details-panel.component.html
  5. 6
      ui-ngx/src/app/modules/home/components/details-panel.component.scss
  6. 56
      ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html
  7. 22
      ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.scss
  8. 65
      ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html
  9. 271
      ui-ngx/src/app/modules/home/pages/rulechain/link-labels.conponent.ts
  10. 25
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html
  11. 111
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts
  12. 28
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.html
  13. 135
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts
  14. 209
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html
  15. 143
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss
  16. 246
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  17. 70
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.tooltipster.scss
  18. 14
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts
  19. 34
      ui-ngx/src/app/shared/models/constants.ts
  20. 57
      ui-ngx/src/app/shared/models/rule-node.models.ts

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

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

1
ui-ngx/angular.json

@ -36,7 +36,6 @@
"node_modules/tooltipster/dist/css/tooltipster.bundle.min.css",
"node_modules/tooltipster/dist/css/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css",
"src/app/shared/components/json-form/react/json-form.scss",
"src/app/modules/home/pages/rulechain/rulechain-page.tooltipster.scss",
"node_modules/rc-select/assets/index.css"
],
"stylePreprocessorOptions": {

18
ui-ngx/src/app/core/http/rule-chain.service.ts

@ -28,7 +28,7 @@ import {
ruleNodeTypeComponentTypes, unknownNodeComponent
} from '@shared/models/rule-chain.models';
import { ComponentDescriptorService } from './component-descriptor.service';
import { RuleNodeComponentDescriptor } from '@app/shared/models/rule-node.models';
import { LinkLabel, RuleNodeComponentDescriptor } from '@app/shared/models/rule-node.models';
import { ResourcesService } from '../services/resources.service';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
@ -144,6 +144,22 @@ export class RuleChainService {
}
}
public getRuleNodeSupportedLinks(component: RuleNodeComponentDescriptor): {[label: string]: LinkLabel} {
const relationTypes = component.configurationDescriptor.nodeDefinition.relationTypes;
const linkLabels: {[label: string]: LinkLabel} = {};
relationTypes.forEach((label) => {
linkLabels[label] = {
name: label,
value: label
};
});
return linkLabels;
}
public ruleNodeAllowCustomLinks(component: RuleNodeComponentDescriptor): boolean {
return component.configurationDescriptor.nodeDefinition.customRelations;
}
private resolveTargetRuleChains(ruleChainConnections: Array<RuleChainConnectionInfo>): Observable<{[ruleChainId: string]: RuleChain}> {
if (ruleChainConnections && ruleChainConnections.length) {
const tasks: Observable<RuleChain>[] = [];

6
ui-ngx/src/app/modules/home/components/details-panel.component.html

@ -16,9 +16,9 @@
-->
<header>
<mat-toolbar color="primary" [ngStyle]="{height: headerHeightPx+'px'}">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" style="height: 100%;">
<div class="mat-toolbar-tools" fxFlex fxLayout="column" fxLayoutAlign="start start">
<mat-toolbar class="details-toolbar" color="primary" [ngStyle]="{height: headerHeightPx+'px'}">
<div class="mat-toolbar-tools" fxFlex fxLayout="row" fxLayoutAlign="start center" style="height: 100%;">
<div class="mat-toolbar-tools tb-details-title-header" fxFlex fxLayout="column" fxLayoutAlign="start start">
<span class="tb-details-title">{{ headerTitle }}</span>
<span class="tb-details-subtitle">{{ headerSubtitle }}</span>
<span style="width: 100%;">

6
ui-ngx/src/app/modules/home/components/details-panel.component.scss

@ -20,6 +20,9 @@
height: 100%;
display: flex;
flex-direction: column;
.mat-toolbar.details-toolbar {
padding: 0;
}
}
:host ::ng-deep {
@ -27,6 +30,9 @@
height: 100%;
min-height: 100px;
max-height: 120px;
&.tb-details-title-header {
min-width: 0;
}
}
.tb-details-title {
width: inherit;

56
ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html

@ -0,0 +1,56 @@
<!--
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.
-->
<form #ruleNodeLinkForm="ngForm" [formGroup]="ruleNodeLinkFormGroup" (ngSubmit)="add()" style="width: 900px;">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>rulenode.add-link</h2>
<span fxFlex></span>
<div [tb-help]="'ruleEngine'"></div>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<tb-rule-node-link
required
formControlName="link"
[allowedLabels]="labels"
[allowCustom]="allowCustomLabels">
</tb-rule-node-link>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || ruleNodeLinkFormGroup.invalid || !ruleNodeLinkFormGroup.dirty">
{{ 'action.add' | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

22
ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.scss

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
tb-rule-node-link {
form {
overflow: hidden !important;
}
}
}

65
ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html

@ -0,0 +1,65 @@
<!--
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.
-->
<mat-form-field [formGroup]="linksFormGroup" style="width: 100%;">
<mat-label translate>rulenode.link-labels</mat-label>
<mat-chip-list #chipList>
<mat-chip
*ngFor="let label of labels"
[selectable]="!disabled"
[removable]="!disabled"
(removed)="remove(label)">
{{label.name}}
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
</mat-chip>
<input matInput type="text" placeholder="{{ !disabled ? ('rulenode.link-label' | translate) : '' }}"
style="max-width: 200px;"
#labelInput
formControlName="label"
matAutocompleteOrigin
#origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin"
[matAutocomplete]="labelAutocomplete"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="add($event)">
</mat-chip-list>
<mat-autocomplete #labelAutocomplete="matAutocomplete"
class="tb-autocomplete"
(optionSelected)="selected($event)"
[displayWith]="displayLabelFn">
<mat-option *ngFor="let label of filteredLabels | async" [value]="label">
<span [innerHTML]="label.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredLabels | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
<span translate>rulenode.no-link-labels-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('rulenode.no-link-label-matching',
{label: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span>
</ng-template>
<span *ngIf="allowCustom">
<a translate (click)="createLinkLabel($event, searchText)">rulenode.create-new-link-label</a>
</span>
</div>
</mat-option>
</mat-autocomplete>
</mat-form-field>

271
ui-ngx/src/app/modules/home/pages/rulechain/link-labels.conponent.ts

@ -0,0 +1,271 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
Component, ElementRef,
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 { 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, startWith } from 'rxjs/operators';
@Component({
selector: 'tb-link-labels',
templateUrl: './link-labels.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => LinkLabelsComponent),
multi: true
}]
})
export class LinkLabelsComponent implements ControlValueAccessor, OnInit, OnChanges {
@ViewChild('chipList', {static: false}) chipList: MatChipList;
@ViewChild('labelAutocomplete', {static: false}) matAutocomplete: MatAutocomplete;
@ViewChild('labelInput', {static: false}) labelInput: ElementRef<HTMLInputElement>;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
private allowCustomValue: boolean;
get allowCustom(): boolean {
return this.allowCustomValue;
}
@Input()
set allowCustom(value: boolean) {
this.allowCustomValue = coerceBooleanProperty(value);
this.separatorKeysCodes = this.allowCustomValue ? [ENTER, COMMA, SEMICOLON] : [];
}
@Input()
allowedLabels: {[label: string]: LinkLabel};
linksFormGroup: FormGroup;
modelValue: Array<string>;
private labelsList: Array<LinkLabel> = [];
separatorKeysCodes: number[] = [];
filteredLabels: Observable<Array<LinkLabel>>;
private labels: Array<LinkLabel> = [];
private searchText = '';
private propagateChange = (v: any) => { };
constructor(private fb: FormBuilder,
public truncate: TruncatePipe,
public translate: TranslateService) {
this.linksFormGroup = this.fb.group({
label: [null]
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit(): void {
this.filteredLabels = this.linksFormGroup.get('label').valueChanges
.pipe(
startWith(''),
map((value) => value ? value : ''),
mergeMap(name => this.fetchLabels(name) ),
share()
);
}
ngOnChanges(changes: SimpleChanges): void {
let reloadLabels = false;
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (change.currentValue !== change.previousValue) {
if (['allowCustom', 'allowedLabels'].includes(propName)) {
reloadLabels = true;
}
}
}
if (reloadLabels) {
this.linksFormGroup.get('label').patchValue('', {emitEvent: true});
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.linksFormGroup.disable({emitEvent: false});
} else {
this.linksFormGroup.enable({emitEvent: false});
}
}
writeValue(value: string[]): void {
this.searchText = '';
this.labelsList.length = 0;
this.labels.length = 0;
this.modelValue = value;
for (const label of Object.keys(this.allowedLabels)) {
this.labelsList.push({name: this.allowedLabels[label].name, value: this.allowedLabels[label].value});
}
if (value) {
value.forEach((label) => {
if (this.allowedLabels[label]) {
this.labels.push(deepClone(this.allowedLabels[label]));
} else {
this.labels.push({
name: label,
value: label
});
}
});
}
if (this.chipList && this.required) {
this.chipList.errorState = this.labels.length ? false : true;
}
this.linksFormGroup.get('label').patchValue('', {emitEvent: true});
}
displayLabelFn(label?: LinkLabel): string | undefined {
return label ? label.name : undefined;
}
textIsNotEmpty(text: string): boolean {
return (text && text != null && text.length > 0) ? true : false;
}
createLinkLabel($event: Event, value: string) {
$event.preventDefault();
this.transformLinkLabel(value);
}
add(event: MatChipInputEvent): void {
if (!this.matAutocomplete.isOpen) {
this.transformLinkLabel(event.value);
}
}
private fetchLabels(searchText?: string): Observable<Array<LinkLabel>> {
this.searchText = searchText;
if (this.searchText && this.searchText.length) {
const search = this.searchText.toUpperCase();
return of(this.labelsList.filter(label => label.name.toUpperCase().includes(search)));
} else {
return of(this.labelsList);
}
}
private transformLinkLabel(value: string) {
if ((value || '').trim()) {
let newLabel: LinkLabel = null;
const labelName = value.trim();
const existingLabel = this.labelsList.find(label => label.name === labelName);
if (existingLabel) {
newLabel = deepClone(existingLabel);
} else if (this.allowCustom) {
newLabel = {
name: labelName,
value: labelName
};
}
if (newLabel) {
this.addLabel(newLabel);
}
}
this.clear('');
}
remove(label: LinkLabel) {
const index = this.labels.indexOf(label);
if (index >= 0) {
this.labels.splice(index, 1);
if (!this.labels.length) {
if (this.required) {
this.chipList.errorState = true;
}
}
this.updateModel();
}
}
selected(event: MatAutocompleteSelectedEvent): void {
this.addLabel(event.option.value);
this.clear('');
}
addLabel(label: LinkLabel): void {
const index = this.labels.findIndex(existinglabel => existinglabel.value === label.value);
if (index === -1) {
this.labels.push(label);
if (this.required) {
this.chipList.errorState = false;
}
}
this.updateModel();
}
clear(value: string = '') {
this.labelInput.nativeElement.value = value;
this.linksFormGroup.get('label').patchValue(null, {emitEvent: true});
setTimeout(() => {
this.labelInput.nativeElement.blur();
this.labelInput.nativeElement.focus();
}, 0);
}
private updateModel() {
const labels = this.labels.map((label => label.value));
this.propagateChange(labels);
}
}

25
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html

@ -1,15 +1,26 @@
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onDeleteRuleNode($event)"
[fxShow]="!isEdit && !isReadOnly">
{{'rulenode.delete' | translate }}
</button>
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="mat-padding" fxLayout="column">
<form #ruleNodeForm="ngForm" [formGroup]="ruleNodeFormGroup">
<fieldset [disabled]="(isLoading$ | async) || !isEdit || isReadOnly">
<section *ngIf="ruleNode.component.type !== ruleNodeType.RULE_CHAIN">
<section fxLayout="column" fxLayout.gt-sm="row">
<mat-form-field class="mat-block">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>rulenode.name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="ruleNodeFormGroup.get('name').hasError('required')">

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

@ -1,38 +1,41 @@
import {
AfterViewInit,
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core';
///
/// 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, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
import { ISearchableComponent } from '@home/models/searchable-component.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, NgForm } from '@angular/forms';
import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { FcRuleNode } from './rulechain-page.models';
import { 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';
@Component({
selector: 'tb-rule-node',
templateUrl: './rule-node-details.component.html',
styleUrls: []
})
export class RuleNodeDetailsComponent extends PageComponent implements OnInit {
export class RuleNodeDetailsComponent extends PageComponent implements OnInit, OnChanges {
@ViewChild('ruleNodeForm', {static: true}) ruleNodeForm: NgForm;
ruleNodeValue: FcRuleNode;
@Input()
set ruleNode(ruleNode: FcRuleNode) {
this.ruleNodeValue = ruleNode;
}
ruleNode: FcRuleNode;
@Input()
ruleChainId: string;
@ -43,28 +46,84 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit {
@Input()
isReadOnly: boolean;
@Output()
deleteRuleNode = new EventEmitter();
ruleNodeType = RuleNodeType;
entityType = EntityType;
ruleNodeFormGroup: FormGroup;
private ruleNodeFormSubscription: Subscription;
constructor(protected store: Store<AppState>,
private fb: FormBuilder) {
private fb: FormBuilder,
private ruleChainService: RuleChainService) {
super(store);
this.ruleNodeFormGroup = this.fb.group({});
}
private buildForm() {
if (this.ruleNodeFormSubscription) {
this.ruleNodeFormSubscription.unsubscribe();
this.ruleNodeFormSubscription = null;
}
if (this.ruleNode) {
if (this.ruleNode.component.type !== RuleNodeType.RULE_CHAIN) {
this.ruleNodeFormGroup = this.fb.group({
name: [this.ruleNode.name, [Validators.required]],
debugMode: [this.ruleNode.debugMode, []],
additionalInfo: this.fb.group(
{
description: [this.ruleNode.additionalInfo ? this.ruleNode.additionalInfo.description : ''],
}
)
});
} else {
this.ruleNodeFormGroup = this.fb.group({
targetRuleChainId: [this.ruleNode.targetRuleChainId, [Validators.required]],
additionalInfo: this.fb.group(
{
description: [this.ruleNode.additionalInfo ? this.ruleNode.additionalInfo.description : ''],
}
)
});
}
this.ruleNodeFormSubscription = this.ruleNodeFormGroup.valueChanges.subscribe(() => {
this.updateRuleNode();
});
} else {
this.ruleNodeFormGroup = this.fb.group({});
}
}
ngOnInit(): void {
private updateRuleNode() {
const formValue = this.ruleNodeFormGroup.value || {};
if (this.ruleNode.component.type === RuleNodeType.RULE_CHAIN) {
const targetRuleChainId: string = formValue.targetRuleChainId;
if (this.ruleNode.targetRuleChainId !== targetRuleChainId && targetRuleChainId) {
this.ruleChainService.getRuleChain(targetRuleChainId).subscribe(
(ruleChain) => {
this.ruleNode.name = ruleChain.name;
Object.assign(this.ruleNode, formValue);
}
);
} else {
Object.assign(this.ruleNode, formValue);
}
} else {
Object.assign(this.ruleNode, formValue);
}
}
onDeleteRuleNode(event) {
this.deleteRuleNode.emit();
ngOnInit(): void {
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (change.currentValue !== change.previousValue) {
if (propName === 'ruleNode') {
this.buildForm();
}
}
}
}
}

28
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.html

@ -0,0 +1,28 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="mat-padding" fxLayout="column">
<form #ruleNodeLinkForm="ngForm" [formGroup]="ruleNodeLinkFormGroup">
<tb-link-labels
fxFlex
required
formControlName="labels"
[allowedLabels]="allowedLabels"
[allowCustom]="allowCustom"
></tb-link-labels>
</form>
</div>

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

@ -0,0 +1,135 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
Component, ElementRef,
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 { 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',
templateUrl: './rule-node-link.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RuleNodeLinkComponent),
multi: true
}]
})
export class RuleNodeLinkComponent implements ControlValueAccessor, OnInit {
@ViewChild('ruleNodeLinkForm', {static: true}) ruleNodeLinkForm: NgForm;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
private allowCustomValue: boolean;
get allowCustom(): boolean {
return this.allowCustomValue;
}
@Input()
set allowCustom(value: boolean) {
this.allowCustomValue = coerceBooleanProperty(value);
}
@Input()
allowedLabels: {[label: string]: LinkLabel};
ruleNodeLinkFormGroup: FormGroup;
modelValue: FcRuleEdge;
private propagateChange = (v: any) => { };
constructor(private fb: FormBuilder,
public truncate: TruncatePipe,
public translate: TranslateService) {
this.ruleNodeLinkFormGroup = this.fb.group({
labels: [[], Validators.required]
});
this.ruleNodeLinkFormGroup.get('labels').valueChanges.subscribe(
(labels: string[]) => this.updateModel(labels)
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit(): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.ruleNodeLinkFormGroup.disable({emitEvent: false});
} else {
this.ruleNodeLinkFormGroup.enable({emitEvent: false});
}
}
writeValue(value: FcRuleEdge): void {
this.modelValue = value;
const labels = this.modelValue && this.modelValue.labels ? this.modelValue.labels : [];
this.ruleNodeLinkFormGroup.get('labels').patchValue(labels, {emitEvent: false});
}
private updateModel(labels: string[]) {
if (labels && labels.length) {
this.modelValue.labels = labels;
this.modelValue.label = labels.join(' / ');
this.propagateChange(this.modelValue);
} else {
this.propagateChange(this.required ? null : this.modelValue);
}
}
}

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

@ -17,14 +17,6 @@
-->
<div class="mat-content" fxFlex tb-fullscreen [fullscreen]="isFullscreen"
fxLayout="column" class="tb-rulechain">
<button mat-button
type="button"
mat-icon-button class="tb-fullscreen-button"
(click)="isFullscreen = !isFullscreen"
matTooltip="{{(isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
<section class="tb-rulechain-container" fxFlex fxLayout="column">
<div class="tb-rulechain-layout" fxFlex fxLayout="row">
<section fxLayout="row"
@ -37,7 +29,7 @@
<mat-icon>menu</mat-icon>
</button>
</section>
<mat-drawer-container style="width: 100%;">
<mat-drawer-container style="width: 100%; height: 100%;">
<mat-drawer class="tb-rulechain-library mat-elevation-z4"
disableClose="true"
mode="side"
@ -96,91 +88,122 @@
</mat-expansion-panel>
</div>
</mat-drawer>
<mat-drawer-content>
<mat-drawer-container style="width: 100%;">
<mat-drawer-content>
<div class="tb-absolute-fill tb-rulechain-graph">
<fc-canvas #ruleChainCanvas
id="tb-rulchain-canvas"
[model]="ruleChainModel"
(modelChanged)="onModelChanged()"
[selectedObjects]="selectedObjects"
[edgeStyle]="flowchartConstants.curvedStyle"
[automaticResize]="true"
[nodeWidth]="170"
[nodeHeight]="50"
[dragAnimation]="flowchartConstants.dragAnimationRepaint"
[userCallbacks]="editCallbacks">
</fc-canvas>
</div>
<section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end">
<button [disabled]="isLoading$ | async"
mat-fab color="accent" class="tb-btn-footer"
[ngClass]="{'tb-hide': !objectsSelected()}"
(click)="deleteSelected()"
matTooltip="{{ 'rulenode.delete-selected-objects' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
<button [disabled]="(isLoading$ | async) || !isDebugModeEnabled()"
mat-fab color="accent" class="tb-btn-footer"
(click)="resetDebugModeInAllNodes()"
matTooltip="{{ 'rulenode.reset-debug-mode' | translate }}"
matTooltipPosition="above">
<mat-icon>bug_report</mat-icon>
</button>
<button [disabled]="(isLoading$ | async) || isInvalid || (!isDirtyValue && !isImport)"
mat-fab color="accent" class="tb-btn-footer"
(click)="saveRuleChain()"
matTooltip="{{ 'action.apply-changes' | translate }}"
matTooltipPosition="above">
<mat-icon>done</mat-icon>
</button>
<button [disabled]="(isLoading$ | async) || !isDirtyValue"
mat-fab color="accent" class="tb-btn-footer"
(click)="revertRuleChain()"
matTooltip="{{ 'action.decline-changes' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</section>
</mat-drawer-content>
<mat-drawer class="tb-details-drawer"
[opened]="isEditingRuleNode"
mode="over"
position="end">
<tb-details-panel fxFlex
headerTitle="{{editingRuleNode?.name}}"
headerSubtitle="{{ ruleNodeTypeDescriptorsMap.get(editingRuleNode.component.type).name | translate }}"
[isReadOnly]="false"
[isAlwaysEdit]="true"
(closeDetails)="onEditRuleNodeClosed()"
(toggleDetailsEditMode)="onRevertRuleNodeEdit()"
(applyDetails)="saveRuleNode()"
[theForm]="tbEditWidget.widgetForm">
<div class="details-buttons">
<div [tb-help]="helpLinkIdForRuleNodeType()"></div>
<mat-drawer class="tb-details-drawer"
[opened]="isEditingRuleNode || isEditingRuleNodeLink"
(closed)="onDetailsDrawerClosed()"
mode="over"
position="end">
<tb-details-panel *ngIf="editingRuleNode" fxFlex
headerTitle="{{editingRuleNode.name}}"
headerSubtitle="{{ ruleNodeTypeDescriptorsMap.get(editingRuleNode.component.type).name | translate }}"
[isReadOnly]="selectedRuleNodeTabIndex > 0"
[isAlwaysEdit]="true"
(closeDetails)="onEditRuleNodeClosed()"
(toggleDetailsEditMode)="onRevertRuleNodeEdit()"
(applyDetails)="saveRuleNode()"
[theForm]="tbRuleNode.ruleNodeForm">
<div class="details-buttons">
<div [tb-help]="helpLinkIdForRuleNodeType()"></div>
</div>
<mat-tab-group fxFlex class="tb-absolute-fill" [(selectedIndex)]="selectedRuleNodeTabIndex">
<mat-tab label="{{ 'rulenode.details' | translate }}">
<tb-rule-node #tbRuleNode
[ruleNode]="editingRuleNode"
[ruleChainId]="ruleChain.id.id"
[isEdit]="true"
[isReadOnly]="false">
</tb-rule-node>
</mat-tab>
<mat-tab *ngIf="editingRuleNode.ruleNodeId" label="{{ 'rulenode.events' | translate }}">
</mat-tab>
<mat-tab label="{{ 'rulenode.help' | translate }}">
<div class="tb-rule-node-help">
<div id="tb-node-content" class="mat-padding" fxLayout="column">
<div class="tb-node-title">{{editingRuleNode.component.name}}</div>
<div>&nbsp;</div>
<div class="tb-node-description">{{editingRuleNode.component.configurationDescriptor.nodeDefinition.description}}</div>
<div>&nbsp;</div>
<div class="tb-node-details" [innerHtml]="editingRuleNode.component.configurationDescriptor.nodeDefinition.details"></div>
</div>
</div>
<mat-tab-group fxFlex class="tb-absolute-fill" [(selectedIndex)]="selectedRuleNodeTabIndex">
<mat-tab label="{{ 'rulenode.details' | translate }}">
</mat-tab>
<mat-tab *ngIf="isEditingRuleNode && editingRuleNode.ruleNodeId" label="{{ 'rulenode.events' | translate }}">
</mat-tab>
<mat-tab label="{{ 'rulenode.help' | translate }}">
<div class="tb-rule-node-help">
<div id="tb-node-content" class="mat-padding" fxLayout="column">
<div class="tb-node-title">{{editingRuleNode.component.name}}</div>
<div>&nbsp;</div>
<div class="tb-node-description">{{editingRuleNode.component.configurationDescriptor.nodeDefinition.description}}</div>
<div>&nbsp;</div>
<div class="tb-node-details" [innerHtml]="editingRuleNode.component.configurationDescriptor.nodeDefinition.details"></div>
</div>
</div>
</mat-tab>
</mat-tab-group>
</tb-details-panel>
</mat-drawer>
</mat-drawer-container>
</mat-tab>
</mat-tab-group>
</tb-details-panel>
<tb-details-panel *ngIf="editingRuleNodeLink" fxFlex
headerTitle="{{editingRuleNodeLink.label}}"
headerSubtitle="{{'rulenode.link-details' | translate}}"
[isReadOnly]="false"
[isAlwaysEdit]="true"
(closeDetails)="onEditRuleNodeLinkClosed()"
(toggleDetailsEditMode)="onRevertRuleNodeLinkEdit()"
(applyDetails)="saveRuleNodeLink()"
[theForm]="tbRuleNodeLink.ruleNodeLinkForm">
<div class="details-buttons">
<div [tb-help]="'ruleEngine'"></div>
</div>
<tb-rule-node-link
fxFlex
#tbRuleNodeLink
[(ngModel)]="editingRuleNodeLink"
[allowedLabels]="editingRuleNodeLinkLabels"
[allowCustom]="editingRuleNodeAllowCustomLabels">
</tb-rule-node-link>
</tb-details-panel>
</mat-drawer>
<mat-drawer-content class="tb-rulechain-graph-content">
<button mat-button
type="button"
mat-icon-button class="tb-fullscreen-button"
(click)="isFullscreen = !isFullscreen"
matTooltip="{{(isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
<div class="tb-absolute-fill tb-rulechain-graph">
<fc-canvas #ruleChainCanvas
id="tb-rulchain-canvas"
[model]="ruleChainModel"
(modelChanged)="onModelChanged()"
[selectedObjects]="selectedObjects"
[edgeStyle]="flowchartConstants.curvedStyle"
[automaticResize]="true"
[nodeWidth]="170"
[nodeHeight]="50"
[dragAnimation]="flowchartConstants.dragAnimationRepaint"
[userCallbacks]="editCallbacks">
</fc-canvas>
</div>
<section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end">
<button [disabled]="isLoading$ | async"
mat-fab color="accent" class="tb-btn-footer"
[ngClass]="{'tb-hide': !objectsSelected()}"
(click)="deleteSelected()"
matTooltip="{{ 'rulenode.delete-selected-objects' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
<button [disabled]="(isLoading$ | async) || !isDebugModeEnabled()"
mat-fab color="accent" class="tb-btn-footer"
(click)="resetDebugModeInAllNodes()"
matTooltip="{{ 'rulenode.reset-debug-mode' | translate }}"
matTooltipPosition="above">
<mat-icon>bug_report</mat-icon>
</button>
<button [disabled]="(isLoading$ | async) || isInvalid || (!isDirtyValue && !isImport)"
mat-fab color="accent" class="tb-btn-footer"
(click)="saveRuleChain()"
matTooltip="{{ 'action.apply-changes' | translate }}"
matTooltipPosition="above">
<mat-icon>done</mat-icon>
</button>
<button [disabled]="(isLoading$ | async) || !isDirtyValue"
mat-fab color="accent" class="tb-btn-footer"
(click)="revertRuleChain()"
matTooltip="{{ 'action.decline-changes' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</section>
</mat-drawer-content>
</mat-drawer-container>
</div>

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

@ -126,8 +126,12 @@
}
}
}
.tb-rulechain-graph {
z-index: 0;
.mat-drawer-content.tb-rulechain-graph-content {
overflow: hidden;
.tb-rulechain-graph {
z-index: 0;
overflow: auto;
}
}
.fc-canvas {
.fc-node {
@ -210,109 +214,56 @@
}
}
/*:host ::ng-deep {
.tb-rulechain {
section.tb-header-buttons.tb-library-open {
.mat-fab {
.mat-button-wrapper {
padding: 0;
}
}
}
.tb-rulechain-library {
.mat-toolbar {
.mat-toolbar-tools {
.mat-form-field {
.mat-form-field-infix {
width: auto;
}
}
}
}
.tb-rulechain-library-panel-group {
.mat-expansion-panel-body {
padding: 0;
}
}
}
}
.fc-canvas {
.fc-node {
border-radius: 8px;
&.fc-selected {
&:not(.fc-edit) {
margin: -3px;
border: solid 3px #f00;
}
}
}
.tb-rule-node-tooltip,
.tb-rule-node-help {
color: #333;
}
.fc-edit {
.fc-nodeedit,
.fc-nodedelete {
border: solid 2px #fff;
background: #f83e05;
outline: none;
}
}
.tb-rule-node-tooltip {
max-width: 300px;
font-size: 14px;
.fc-arrow-marker {
polygon {
fill: #808080;
stroke: #808080;
}
}
.fc-arrow-marker-selected {
polygon {
fill: #f00;
stroke: #f00;
}
}
&.tb-lib-tooltip {
width: 300px;
}
}
.fc-edge {
outline: none;
stroke: #808080;
.tb-rule-node-help {
font-size: 16px;
}
&.fc-selected {
stroke: #f00;
}
.tb-rule-node-error-tooltip {
font-size: 16px;
color: #ea0d0d;
}
&.fc-hover {
stroke: #808080;
}
.tb-rule-node-tooltip,
.tb-rule-node-error-tooltip,
.tb-rule-node-help {
#tb-node-content {
.tb-node-title {
font-weight: 600;
}
.edge-endpoint {
fill: #808080;
.tb-node-description {
font-style: italic;
color: #555;
}
.fc-edge-label {
opacity: 1 !important;
&:focus {
outline: 0;
}
&.fc-selected {
.fc-edge-label-text {
span {
color: #fff !important;
background-color: #f00 !important;
border: solid #f00 !important;
}
}
}
.fc-edge-label-text {
font-size: 14px !important;
font-weight: 600 !important;
.tb-node-details {
padding-top: 10px;
padding-bottom: 10px;
}
span {
background-color: #fff !important;
color: #003a79 !important;
border: solid 2px #003a79 !important;
}
}
code {
padding: 0 3px 2px 3px;
margin: 1px;
font-size: 12px;
color: #ad1625;
white-space: nowrap;
background-color: #f7f7f9;
border: 1px solid #e1e1e8;
border-radius: 2px;
}
}
}*/
}

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

@ -18,20 +18,20 @@ import {
AfterViewInit,
Component,
ElementRef,
HostBinding, Inject,
OnInit,
QueryList,
QueryList, SkipSelf,
ViewChild,
ViewChildren,
ViewEncapsulation,
HostBinding
ViewEncapsulation
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder } from '@angular/forms';
import { 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 } from '@angular/material';
import { MatDialog, MatExpansionPanel, ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { DialogService } from '@core/services/dialog.service';
import { AuthService } from '@core/auth/auth.service';
import { ActivatedRoute, Router } from '@angular/router';
@ -43,6 +43,8 @@ import {
} from '@shared/models/rule-chain.models';
import { FlowchartConstants, NgxFlowchartComponent, UserCallbacks } from 'ngx-flowchart/dist/ngx-flowchart';
import {
getRuleNodeHelpLink,
LinkLabel,
RuleNodeComponentDescriptor,
RuleNodeType,
ruleNodeTypeDescriptors,
@ -50,10 +52,21 @@ import {
} from '@shared/models/rule-node.models';
import { FcRuleEdge, FcRuleNode, FcRuleNodeModel, FcRuleNodeType, FcRuleNodeTypeModel } from './rulechain-page.models';
import { RuleChainService } from '@core/http/rule-chain.service';
import { fromEvent, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
import { fromEvent, never, of, throwError, NEVER, Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, tap, mergeMap } from 'rxjs/operators';
import { ISearchableComponent } from '../../models/searchable-component.models';
import { deepClone, isDefined, isString } 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';
@Component({
selector: 'tb-rulechain-page',
@ -88,11 +101,19 @@ export class RuleChainPageComponent extends PageComponent
errorTooltips: {[nodeId: string]: JQueryTooltipster.ITooltipsterInstance} = {};
isFullscreen = false;
editingRuleNode = null;
selectedRuleNodeTabIndex = 0;
editingRuleNode: FcRuleNode = null;
isEditingRuleNode = false;
editingRuleNodeIndex = -1;
editingRuleNodeAllowCustomLabels = false;
editingRuleNodeLinkLabels: {[label: string]: LinkLabel};
editingRuleNodeLink = null;
@ViewChild('tbRuleNode', {static: false}) ruleNodeComponent: RuleNodeDetailsComponent;
@ViewChild('tbRuleNodeLink', {static: false}) ruleNodeLinkComponent: RuleNodeLinkComponent;
editingRuleNodeLink: FcRuleEdge = null;
isEditingRuleNodeLink = false;
editingRuleNodeLinkIndex = -1;
isLibraryOpen = true;
@ -110,17 +131,17 @@ export class RuleChainPageComponent extends PageComponent
editCallbacks: UserCallbacks = {
edgeDoubleClick: (event, edge) => {
console.log('TODO');
this.openLinkDetails(edge);
},
edgeEdit: (event, edge) => {
console.log('TODO');
this.openLinkDetails(edge);
},
nodeCallbacks: {
doubleClick: (event, node) => {
console.log('TODO');
doubleClick: (event, node: FcRuleNode) => {
this.openNodeDetails(node);
},
nodeEdit: (event, node) => {
console.log('TODO');
nodeEdit: (event, node: FcRuleNode) => {
this.openNodeDetails(node);
},
mouseEnter: this.displayNodeDescriptionTooltip.bind(this),
mouseLeave: this.destroyTooltips.bind(this),
@ -129,9 +150,40 @@ export class RuleChainPageComponent extends PageComponent
isValidEdge: (source, destination) => {
return source.type === FlowchartConstants.rightConnectorType && destination.type === FlowchartConstants.leftConnectorType;
},
createEdge: (event, edge) => {
createEdge: (event, edge: FcRuleEdge) => {
console.log('TODO');
return of(edge);
const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source) as FcRuleNode;
if (sourceNode.component.type === RuleNodeType.INPUT) {
const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination) as FcRuleNode;
if (destNode.component.type === RuleNodeType.RULE_CHAIN) {
return NEVER;
} else {
const found = this.ruleChainModel.edges.find(theEdge => theEdge.source === (this.inputConnectorId + ''));
if (found) {
this.ruleChainCanvas.modelService.edges.delete(found);
}
return of(edge);
}
} else {
if (edge.label) {
if (!edge.labels) {
edge.labels = edge.label.split(' / ');
}
return of(edge);
} else {
const labels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
const allowCustomLabels = this.ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component);
return this.addRuleNodeLink(edge, labels, allowCustomLabels).pipe(
mergeMap((res) => {
if (res) {
return of(res);
} else {
return NEVER;
}
})
);
}
}
},
dropNode: (event, node) => {
console.log('TODO dropNode');
@ -470,6 +522,88 @@ export class RuleChainPageComponent extends PageComponent
this.validate();
}
helpLinkIdForRuleNodeType(): string {
let component: RuleNodeComponentDescriptor = null;
if (this.editingRuleNode) {
component = this.editingRuleNode.component;
}
return getRuleNodeHelpLink(component);
}
openNodeDetails(node: FcRuleNode) {
if (node.component.type !== RuleNodeType.INPUT) {
this.isEditingRuleNodeLink = false;
this.editingRuleNodeLink = null;
this.isEditingRuleNode = true;
this.editingRuleNodeIndex = this.ruleChainModel.nodes.indexOf(node);
this.editingRuleNode = deepClone(node);
setTimeout(() => {
this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine();
}, 0);
}
}
openLinkDetails(edge: FcRuleEdge) {
const sourceNode: FcRuleNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source) as FcRuleNode;
if (sourceNode.component.type !== RuleNodeType.INPUT) {
this.isEditingRuleNode = false;
this.editingRuleNode = null;
this.editingRuleNodeLinkLabels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
this.editingRuleNodeAllowCustomLabels = this.ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component);
this.isEditingRuleNodeLink = true;
this.editingRuleNodeLinkIndex = this.ruleChainModel.edges.indexOf(edge);
this.editingRuleNodeLink = deepClone(edge);
setTimeout(() => {
this.ruleNodeLinkComponent.ruleNodeLinkFormGroup.markAsPristine();
}, 0);
}
}
onDetailsDrawerClosed() {
this.onEditRuleNodeClosed();
this.onEditRuleNodeLinkClosed();
}
onEditRuleNodeClosed() {
this.editingRuleNode = null;
this.isEditingRuleNode = false;
}
onEditRuleNodeLinkClosed() {
this.editingRuleNodeLink = null;
this.isEditingRuleNodeLink = false;
}
onRevertRuleNodeEdit() {
this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine();
const node = this.ruleChainModel.nodes[this.editingRuleNodeIndex];
this.editingRuleNode = deepClone(node);
}
onRevertRuleNodeLinkEdit() {
this.ruleNodeLinkComponent.ruleNodeLinkFormGroup.markAsPristine();
const edge = this.ruleChainModel.edges[this.editingRuleNodeLinkIndex];
this.editingRuleNodeLink = deepClone(edge);
}
saveRuleNode() {
this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine();
if (this.editingRuleNode.error) {
delete this.editingRuleNode.error;
}
this.ruleChainModel.nodes[this.editingRuleNodeIndex] = this.editingRuleNode;
this.editingRuleNode = deepClone(this.editingRuleNode);
this.onModelChanged();
this.updateRuleNodesHighlight();
}
saveRuleNodeLink() {
this.ruleNodeLinkComponent.ruleNodeLinkFormGroup.markAsPristine();
this.ruleChainModel.edges[this.editingRuleNodeLinkIndex] = this.editingRuleNodeLink;
this.editingRuleNodeLink = deepClone(this.editingRuleNodeLink);
this.onModelChanged();
}
typeHeaderMouseEnter(event: MouseEvent, ruleNodeType: RuleNodeType) {
const type = ruleNodeTypeDescriptors.get(ruleNodeType);
this.displayTooltip(event,
@ -568,11 +702,16 @@ export class RuleChainPageComponent extends PageComponent
}
resetDebugModeInAllNodes() {
let changed = false;
this.ruleChainModel.nodes.forEach((node) => {
if (node.component.type !== RuleNodeType.INPUT && node.component.type !== RuleNodeType.RULE_CHAIN) {
changed = changed || node.debugMode;
node.debugMode = false;
}
});
if (changed) {
this.onModelChanged();
}
}
validate() {
@ -595,6 +734,19 @@ export class RuleChainPageComponent extends PageComponent
this.createRuleChainModel();
}
addRuleNodeLink(link: FcRuleEdge, labels: {[label: string]: LinkLabel}, allowCustomLabels: boolean): Observable<FcRuleEdge> {
return this.dialog.open<AddRuleNodeLinkDialogComponent, AddRuleNodeLinkDialogData,
FcRuleEdge>(AddRuleNodeLinkDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
link,
labels,
allowCustomLabels
}
}).afterClosed();
}
private updateNodeErrorTooltip(node: FcRuleNode) {
if (node.error) {
const element = $('#' + node.id);
@ -670,6 +822,66 @@ export class RuleChainPageComponent extends PageComponent
tooltip.open();
}, 500);
}
}
export interface AddRuleNodeLinkDialogData {
link: FcRuleEdge;
labels: {[label: string]: LinkLabel};
allowCustomLabels: boolean;
}
@Component({
selector: 'tb-add-rule-node-link-dialog',
templateUrl: './add-rule-node-link-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AddRuleNodeLinkDialogComponent}],
styleUrls: ['./add-rule-node-link-dialog.component.scss']
})
export class AddRuleNodeLinkDialogComponent extends DialogComponent<AddRuleNodeLinkDialogComponent, FcRuleEdge>
implements OnInit, ErrorStateMatcher {
ruleNodeLinkFormGroup: FormGroup;
link: FcRuleEdge;
labels: {[label: string]: LinkLabel};
allowCustomLabels: boolean;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddRuleNodeLinkDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddRuleNodeLinkDialogComponent, FcRuleEdge>,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.link = this.data.link;
this.labels = this.data.labels;
this.allowCustomLabels = this.data.allowCustomLabels;
this.ruleNodeLinkFormGroup = this.fb.group({
link: [deepClone(this.link), [Validators.required]]
}
);
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
add(): void {
this.submitted = true;
const link: FcRuleEdge = this.ruleNodeLinkFormGroup.get('link').value;
this.link = {...this.link, ...link};
this.dialogRef.close(this.link);
}
}

70
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.tooltipster.scss

@ -1,70 +0,0 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tooltipster-base {
.tb-rule-node-tooltip,
.tb-rule-node-help {
color: #333;
}
.tb-rule-node-tooltip {
max-width: 300px;
font-size: 14px;
&.tb-lib-tooltip {
width: 300px;
}
}
.tb-rule-node-help {
font-size: 16px;
}
.tb-rule-node-error-tooltip {
font-size: 16px;
color: #ea0d0d;
}
.tb-rule-node-tooltip,
.tb-rule-node-error-tooltip,
.tb-rule-node-help {
#tb-node-content {
.tb-node-title {
font-weight: 600;
}
.tb-node-description {
font-style: italic;
color: #555;
}
.tb-node-details {
padding-top: 10px;
padding-bottom: 10px;
}
code {
padding: 0 3px 2px 3px;
margin: 1px;
font-size: 12px;
color: #ad1625;
white-space: nowrap;
background-color: #f7f7f9;
border: 1px solid #e1e1e8;
border-radius: 2px;
}
}
}
}

14
ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts

@ -21,21 +21,29 @@ import {RuleChainComponent} from '@modules/home/pages/rulechain/rulechain.compon
import {RuleChainRoutingModule} from '@modules/home/pages/rulechain/rulechain-routing.module';
import {HomeComponentsModule} from '@modules/home/components/home-components.module';
import { RuleChainTabsComponent } from '@home/pages/rulechain/rulechain-tabs.component';
import { RuleChainPageComponent } from './rulechain-page.component';
import { RuleChainPageComponent, AddRuleNodeLinkDialogComponent } from './rulechain-page.component';
import { RuleNodeComponent } from '@home/pages/rulechain/rulenode.component';
import { FC_NODE_COMPONENT_CONFIG } from 'ngx-flowchart/dist/ngx-flowchart';
import { RuleNodeDetailsComponent } from './rule-node-details.component';
import { RuleNodeLinkComponent } from './rule-node-link.component';
import { LinkLabelsComponent } from '@home/pages/rulechain/link-labels.conponent';
@NgModule({
entryComponents: [
RuleChainComponent,
RuleChainTabsComponent,
RuleNodeComponent
RuleNodeComponent,
AddRuleNodeLinkDialogComponent
],
declarations: [
RuleChainComponent,
RuleChainTabsComponent,
RuleChainPageComponent,
RuleNodeComponent
RuleNodeComponent,
RuleNodeDetailsComponent,
LinkLabelsComponent,
RuleNodeLinkComponent,
AddRuleNodeLinkDialogComponent
],
providers: [
{

34
ui-ngx/src/app/shared/models/constants.ts

@ -57,6 +57,40 @@ export const HelpLinks = {
linksMap: {
outgoingMailSettings: helpBaseUrl + '/docs/user-guide/ui/mail-settings',
securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings',
ruleEngine: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/overview/',
ruleNodeCheckRelation: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node',
ruleNodeJsFilter: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#script-filter-node',
ruleNodeJsSwitch: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#switch-node',
ruleNodeMessageTypeFilter: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#message-type-filter-node',
ruleNodeMessageTypeSwitch: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#message-type-switch-node',
ruleNodeOriginatorTypeFilter: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#originator-type-filter-node',
ruleNodeOriginatorTypeSwitch: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#originator-type-switch-node',
ruleNodeOriginatorAttributes: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#originator-attributes',
ruleNodeOriginatorFields: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#originator-fields',
ruleNodeCustomerAttributes: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#customer-attributes',
ruleNodeDeviceAttributes: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#device-attributes',
ruleNodeRelatedAttributes: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#related-attributes',
ruleNodeTenantAttributes: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#tenant-attributes',
ruleNodeChangeOriginator: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/transformation-nodes/#change-originator',
ruleNodeTransformMsg: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/transformation-nodes/#script-transformation-node',
ruleNodeMsgToEmail: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/transformation-nodes/#to-email-node',
ruleNodeClearAlarm: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#clear-alarm-node',
ruleNodeCreateAlarm: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#create-alarm-node',
ruleNodeMsgDelay: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#delay-node',
ruleNodeMsgGenerator: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#generator-node',
ruleNodeLog: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#log-node',
ruleNodeRpcCallReply: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#rpc-call-reply-node',
ruleNodeRpcCallRequest: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#rpc-call-request-node',
ruleNodeSaveAttributes: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#save-attributes-node',
ruleNodeSaveTimeseries: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#save-timeseries-node',
ruleNodeRuleChain: helpBaseUrl + '/docs/user-guide/ui/rule-chains/',
ruleNodeAwsSns: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#aws-sns-node',
ruleNodeAwsSqs: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#aws-sqs-node',
ruleNodeKafka: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#kafka-node',
ruleNodeMqtt: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#mqtt-node',
ruleNodeRabbitMq: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#rabbitmq-node',
ruleNodeRestApiCall: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#rest-api-call-node',
ruleNodeSendEmail: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#send-email-node',
tenants: helpBaseUrl + '/docs/user-guide/ui/tenants',
customers: helpBaseUrl + '/docs/user-guide/customers',
users: helpBaseUrl + '/docs/user-guide/ui/users',

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

@ -43,6 +43,11 @@ export interface RuleNode extends BaseData<RuleNodeId> {
additionalInfo?: any;
}
export interface LinkLabel {
name: string;
value: string;
}
export interface RuleNodeConfigurationDescriptor {
nodeDefinition: {
description: string;
@ -54,6 +59,7 @@ export interface RuleNodeConfigurationDescriptor {
defaultConfiguration: any;
icon?: string;
iconUrl?: string;
docUrl?: string;
uiResources?: string[];
uiResourceLoadError?: string;
};
@ -178,3 +184,54 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor {
type: RuleNodeType;
configurationDescriptor?: RuleNodeConfigurationDescriptor;
}
const ruleNodeClazzHelpLinkMap = {
'org.thingsboard.rule.engine.filter.TbCheckRelationNode': 'ruleNodeCheckRelation',
'org.thingsboard.rule.engine.filter.TbJsFilterNode': 'ruleNodeJsFilter',
'org.thingsboard.rule.engine.filter.TbJsSwitchNode': 'ruleNodeJsSwitch',
'org.thingsboard.rule.engine.filter.TbMsgTypeFilterNode': 'ruleNodeMessageTypeFilter',
'org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode': 'ruleNodeMessageTypeSwitch',
'org.thingsboard.rule.engine.filter.TbOriginatorTypeFilterNode': 'ruleNodeOriginatorTypeFilter',
'org.thingsboard.rule.engine.filter.TbOriginatorTypeSwitchNode': 'ruleNodeOriginatorTypeSwitch',
'org.thingsboard.rule.engine.metadata.TbGetAttributesNode': 'ruleNodeOriginatorAttributes',
'org.thingsboard.rule.engine.metadata.TbGetOriginatorFieldsNode': 'ruleNodeOriginatorFields',
'org.thingsboard.rule.engine.metadata.TbGetCustomerAttributeNode': 'ruleNodeCustomerAttributes',
'org.thingsboard.rule.engine.metadata.TbGetDeviceAttrNode': 'ruleNodeDeviceAttributes',
'org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode': 'ruleNodeRelatedAttributes',
'org.thingsboard.rule.engine.metadata.TbGetTenantAttributeNode': 'ruleNodeTenantAttributes',
'org.thingsboard.rule.engine.transform.TbChangeOriginatorNode': 'ruleNodeChangeOriginator',
'org.thingsboard.rule.engine.transform.TbTransformMsgNode': 'ruleNodeTransformMsg',
'org.thingsboard.rule.engine.mail.TbMsgToEmailNode': 'ruleNodeMsgToEmail',
'org.thingsboard.rule.engine.action.TbClearAlarmNode': 'ruleNodeClearAlarm',
'org.thingsboard.rule.engine.action.TbCreateAlarmNode': 'ruleNodeCreateAlarm',
'org.thingsboard.rule.engine.delay.TbMsgDelayNode': 'ruleNodeMsgDelay',
'org.thingsboard.rule.engine.debug.TbMsgGeneratorNode': 'ruleNodeMsgGenerator',
'org.thingsboard.rule.engine.action.TbLogNode': 'ruleNodeLog',
'org.thingsboard.rule.engine.rpc.TbSendRPCReplyNode': 'ruleNodeRpcCallReply',
'org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode': 'ruleNodeRpcCallRequest',
'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode': 'ruleNodeSaveAttributes',
'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode': 'ruleNodeSaveTimeseries',
'tb.internal.RuleChain': 'ruleNodeRuleChain',
'org.thingsboard.rule.engine.aws.sns.TbSnsNode': 'ruleNodeAwsSns',
'org.thingsboard.rule.engine.aws.sqs.TbSqsNode': 'ruleNodeAwsSqs',
'org.thingsboard.rule.engine.kafka.TbKafkaNode': 'ruleNodeKafka',
'org.thingsboard.rule.engine.mqtt.TbMqttNode': 'ruleNodeMqtt',
'org.thingsboard.rule.engine.rabbitmq.TbRabbitMqNode': 'ruleNodeRabbitMq',
'org.thingsboard.rule.engine.rest.TbRestApiCallNode': 'ruleNodeRestApiCall',
'org.thingsboard.rule.engine.mail.TbSendEmailNode': 'ruleNodeSendEmail'
};
export function getRuleNodeHelpLink(component: RuleNodeComponentDescriptor): string {
if (component) {
if (component.configurationDescriptor &&
component.configurationDescriptor.nodeDefinition &&
component.configurationDescriptor.nodeDefinition.docUrl) {
return component.configurationDescriptor.nodeDefinition.docUrl;
} else if (component.clazz) {
if (ruleNodeClazzHelpLinkMap[component.clazz]) {
return ruleNodeClazzHelpLinkMap[component.clazz];
}
}
}
return 'ruleEngine';
}

Loading…
Cancel
Save