From 70e81e84ebe2a2748cff09d9a1967a0eb074f7b9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 17 Dec 2019 20:16:40 +0200 Subject: [PATCH] RuleChain UI --- msa/js-executor/package-lock.json | 23 +- ui-ngx/angular.json | 1 - .../src/app/core/http/rule-chain.service.ts | 18 +- .../components/details-panel.component.html | 6 +- .../components/details-panel.component.scss | 6 + .../add-rule-node-link-dialog.component.html | 56 ++++ .../add-rule-node-link-dialog.component.scss | 22 ++ .../rulechain/link-labels.component.html | 65 +++++ .../pages/rulechain/link-labels.conponent.ts | 271 ++++++++++++++++++ .../rule-node-details.component.html | 25 +- .../rulechain/rule-node-details.component.ts | 111 +++++-- .../rulechain/rule-node-link.component.html | 28 ++ .../rulechain/rule-node-link.component.ts | 135 +++++++++ .../rulechain/rulechain-page.component.html | 209 ++++++++------ .../rulechain/rulechain-page.component.scss | 143 +++------ .../rulechain/rulechain-page.component.ts | 246 ++++++++++++++-- .../rulechain/rulechain-page.tooltipster.scss | 70 ----- .../home/pages/rulechain/rulechain.module.ts | 14 +- ui-ngx/src/app/shared/models/constants.ts | 34 +++ .../src/app/shared/models/rule-node.models.ts | 57 ++++ 20 files changed, 1217 insertions(+), 323 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/link-labels.conponent.ts create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts delete mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.tooltipster.scss diff --git a/msa/js-executor/package-lock.json b/msa/js-executor/package-lock.json index c61ef011a2..bc9fdf512d 100644 --- a/msa/js-executor/package-lock.json +++ b/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", diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index a01dbb4b75..c68e36a90b 100644 --- a/ui-ngx/angular.json +++ b/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": { diff --git a/ui-ngx/src/app/core/http/rule-chain.service.ts b/ui-ngx/src/app/core/http/rule-chain.service.ts index a6ff059659..8619c0e3f6 100644 --- a/ui-ngx/src/app/core/http/rule-chain.service.ts +++ b/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): Observable<{[ruleChainId: string]: RuleChain}> { if (ruleChainConnections && ruleChainConnections.length) { const tasks: Observable[] = []; diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.html b/ui-ngx/src/app/modules/home/components/details-panel.component.html index 069f9d5aff..d489ccef2a 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.html @@ -16,9 +16,9 @@ -->
- -
-
+ +
+
{{ headerTitle }} {{ headerSubtitle }} diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.scss b/ui-ngx/src/app/modules/home/components/details-panel.component.scss index b8367a6777..65119c89c7 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.scss +++ b/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; diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html new file mode 100644 index 0000000000..832ecbd9ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html @@ -0,0 +1,56 @@ + +
+ +

rulenode.add-link

+ +
+ +
+ + +
+
+ + +
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.scss new file mode 100644 index 0000000000..5433dd0be9 --- /dev/null +++ b/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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html new file mode 100644 index 0000000000..5dd617479c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html @@ -0,0 +1,65 @@ + + + rulenode.link-labels + + + {{label.name}} + close + + + + + + + + +
+
+ rulenode.no-link-labels-found +
+ + + {{ translate.get('rulenode.no-link-label-matching', + {label: truncate.transform(searchText, true, 6, '...')}) | async }} + + + + rulenode.create-new-link-label + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.conponent.ts b/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.conponent.ts new file mode 100644 index 0000000000..c7303f145f --- /dev/null +++ b/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; + + 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; + + private labelsList: Array = []; + + separatorKeysCodes: number[] = []; + + filteredLabels: Observable>; + + private labels: Array = []; + + 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> { + 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); + } +} + diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html index 4770b90fe3..1d27fcf7f5 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html @@ -1,15 +1,26 @@ - +
- + rulenode.name diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts index b3995a5f09..8a3f654824 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts +++ b/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, - 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(); + } + } + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.html new file mode 100644 index 0000000000..4b54c92e6b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.html @@ -0,0 +1,28 @@ + +
+ + + +
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts new file mode 100644 index 0000000000..7b3f6ead9d --- /dev/null +++ b/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); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html index 0b0a7507cc..8e86f155ea 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html @@ -17,14 +17,6 @@ -->
-
menu
- +
- - - -
- - -
- -
- - -
-
+ + +
+
+
+ + + + + + + + +
+
+
{{editingRuleNode.component.name}}
+
 
+
{{editingRuleNode.component.configurationDescriptor.nodeDefinition.description}}
+
 
+
+
- - - - - - -
-
-
{{editingRuleNode.component.name}}
-
 
-
{{editingRuleNode.component.configurationDescriptor.nodeDefinition.description}}
-
 
-
-
-
-
-
-
-
- + + + + +
+
+
+ + +
+ + + +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss index 4a8fbeb404..e161ded881 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss +++ b/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; } } -}*/ +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 3d1d0d8712..04e86b6090 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/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 { + return this.dialog.open(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 + implements OnInit, ErrorStateMatcher { + + ruleNodeLinkFormGroup: FormGroup; + + link: FcRuleEdge; + labels: {[label: string]: LinkLabel}; + allowCustomLabels: boolean; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddRuleNodeLinkDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + 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); + } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.tooltipster.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.tooltipster.scss deleted file mode 100644 index afb53cdeff..0000000000 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.tooltipster.scss +++ /dev/null @@ -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; - } - } - } -} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts index 1b961517fa..665d287320 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts +++ b/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: [ { diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 96a4fe782c..978cec592d 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/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', diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index 2aa77791f6..9d122bb709 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -43,6 +43,11 @@ export interface RuleNode extends BaseData { 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'; +}