From cabdc68a429c5db9c852979d1486a5ad22f47092 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 22 Nov 2019 17:58:13 +0200 Subject: [PATCH] RuleChain page implementation. --- ui-ngx/proxy.conf.json | 4 + .../core/http/component-descriptor.service.ts | 103 +++++ .../src/app/core/http/rule-chain.service.ts | 189 +++++++- .../rulechain/rulechain-page.component.html | 4 +- .../rulechain/rulechain-page.component.ts | 425 +++++++++++++----- .../pages/rulechain/rulechain-page.models.ts | 49 ++ .../rulechain/rulechain-routing.module.ts | 80 +++- .../models/component-descriptor.models.ts | 39 ++ .../app/shared/models/rule-chain.models.ts | 89 +++- .../src/app/shared/models/rule-node.models.ts | 131 +++++- 10 files changed, 972 insertions(+), 141 deletions(-) create mode 100644 ui-ngx/src/app/core/http/component-descriptor.service.ts create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.models.ts create mode 100644 ui-ngx/src/app/shared/models/component-descriptor.models.ts diff --git a/ui-ngx/proxy.conf.json b/ui-ngx/proxy.conf.json index 5c77d20da8..a558aee24e 100644 --- a/ui-ngx/proxy.conf.json +++ b/ui-ngx/proxy.conf.json @@ -3,6 +3,10 @@ "target": "http://localhost:8080", "secure": false }, + "/static/rulenode": { + "target": "http://localhost:8080", + "secure": false + }, "/api/ws": { "target": "ws://localhost:8080", "ws": true diff --git a/ui-ngx/src/app/core/http/component-descriptor.service.ts b/ui-ngx/src/app/core/http/component-descriptor.service.ts new file mode 100644 index 0000000000..6893a0cd04 --- /dev/null +++ b/ui-ngx/src/app/core/http/component-descriptor.service.ts @@ -0,0 +1,103 @@ +/// +/// 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 { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { ComponentDescriptor, ComponentType } from '@shared/models/component-descriptor.models'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable, of } from 'rxjs'; +import { RuleChainMetaData } from '@shared/models/rule-chain.models'; +import { tap, map } from 'rxjs/operators'; +import { RuleNodeType } from '@shared/models/rule-node.models'; + +@Injectable({ + providedIn: 'root' +}) +export class ComponentDescriptorService { + + private componentsByType: Map> = + new Map>(); + private componentsByClazz: Map = new Map(); + + constructor( + private http: HttpClient + ) { + } + + public getComponentDescriptorsByType(componentType: ComponentType, config?: RequestConfig): Observable> { + const existing = this.componentsByType.get(componentType); + if (existing) { + return of(existing); + } else { + return this.http.get>(`/api/components/${componentType}`, defaultHttpOptionsFromConfig(config)).pipe( + map((componentDescriptors) => { + this.componentsByType.set(componentType, componentDescriptors); + componentDescriptors.forEach((componentDescriptor) => { + this.componentsByClazz.set(componentDescriptor.clazz, componentDescriptor); + }); + return componentDescriptors; + }) + ); + } + } + + public getComponentDescriptorsByTypes(componentTypes: Array, + config?: RequestConfig): Observable> { + let result: ComponentDescriptor[] = []; + for (let i = componentTypes.length - 1; i >= 0; i--) { + const componentType = componentTypes[i]; + const componentDescriptors = this.componentsByType.get(componentType); + if (componentDescriptors) { + result = result.concat(componentDescriptors); + componentTypes.splice(i, 1); + } + } + if (!componentTypes.length) { + return of(result); + } else { + return this.http.get>(`/api/components?componentTypes=${componentTypes.join(',')}`, + defaultHttpOptionsFromConfig(config)).pipe( + map((componentDescriptors) => { + componentDescriptors.forEach((componentDescriptor) => { + let componentsList = this.componentsByType.get(componentDescriptor.type); + if (!componentsList) { + componentsList = new Array(); + this.componentsByType.set(componentDescriptor.type, componentsList); + } + componentsList.push(componentDescriptor); + this.componentsByClazz.set(componentDescriptor.clazz, componentDescriptor); + }); + result = result.concat(componentDescriptors); + return result; + }) + ); + } + } + + public getComponentDescriptorByClazz(componentDescriptorClazz: string, config?: RequestConfig): Observable { + const existing = this.componentsByClazz.get(componentDescriptorClazz); + if (existing) { + return of(existing); + } else { + return this.http.get(`/api/component/${componentDescriptorClazz}`, defaultHttpOptionsFromConfig(config)).pipe( + map((componentDescriptor) => { + this.componentsByClazz.set(componentDescriptorClazz, componentDescriptor); + return componentDescriptor; + }) + ); + } + } +} 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 f1379c8e6a..a6ff059659 100644 --- a/ui-ngx/src/app/core/http/rule-chain.service.ts +++ b/ui-ngx/src/app/core/http/rule-chain.service.ts @@ -14,21 +14,39 @@ /// limitations under the License. /// -import {Injectable} from '@angular/core'; -import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; -import {Observable} from 'rxjs/index'; -import {HttpClient} from '@angular/common/http'; -import {PageLink} from '@shared/models/page/page-link'; -import {PageData} from '@shared/models/page/page-data'; -import {RuleChain} from '@shared/models/rule-chain.models'; +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { forkJoin, Observable, of } from 'rxjs/index'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { + ResolvedRuleChainMetaData, + RuleChain, RuleChainConnectionInfo, + RuleChainMetaData, + ruleChainNodeComponent, + ruleNodeTypeComponentTypes, unknownNodeComponent +} from '@shared/models/rule-chain.models'; +import { ComponentDescriptorService } from './component-descriptor.service'; +import { 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'; +import { EntityType } from '@shared/models/entity-type.models'; +import { deepClone } from '@core/utils'; @Injectable({ providedIn: 'root' }) export class RuleChainService { + private ruleNodeComponents: Array; + constructor( - private http: HttpClient + private http: HttpClient, + private componentDescriptorService: ComponentDescriptorService, + private resourcesService: ResourcesService, + private translate: TranslateService ) { } public getRuleChains(pageLink: PageLink, config?: RequestConfig): Observable> { @@ -52,4 +70,159 @@ export class RuleChainService { return this.http.post(`/api/ruleChain/${ruleChainId}/root`, null, defaultHttpOptionsFromConfig(config)); } + public getRuleChainMetadata(ruleChainId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/ruleChain/${ruleChainId}/metadata`, defaultHttpOptionsFromConfig(config)); + } + + public getResolvedRuleChainMetadata(ruleChainId: string, config?: RequestConfig): Observable { + return this.getRuleChainMetadata(ruleChainId, config).pipe( + mergeMap((ruleChainMetaData) => { + return this.resolveTargetRuleChains(ruleChainMetaData.ruleChainConnections).pipe( + map((targetRuleChainsMap) => { + const resolvedRuleChainMetadata: ResolvedRuleChainMetaData = {...ruleChainMetaData, targetRuleChainsMap}; + return resolvedRuleChainMetadata; + }) + ); + }) + ); + } + + public saveRuleChainMetadata(ruleChainMetaData: RuleChainMetaData, config?: RequestConfig): Observable { + return this.http.post('/api/ruleChain/metadata', ruleChainMetaData, defaultHttpOptionsFromConfig(config)); + } + + public saveAndGetResolvedRuleChainMetadata(ruleChainMetaData: RuleChainMetaData, + config?: RequestConfig): Observable { + return this.saveRuleChainMetadata(ruleChainMetaData, config).pipe( + mergeMap((savedRuleChainMetaData) => { + return this.resolveTargetRuleChains(savedRuleChainMetaData.ruleChainConnections).pipe( + map((targetRuleChainsMap) => { + const resolvedRuleChainMetadata: ResolvedRuleChainMetaData = {...savedRuleChainMetaData, targetRuleChainsMap}; + return resolvedRuleChainMetadata; + }) + ); + }) + ); + } + + public getRuleNodeComponents(config?: RequestConfig): Observable> { + if (this.ruleNodeComponents) { + return of(this.ruleNodeComponents); + } else { + return this.loadRuleNodeComponents(config).pipe( + mergeMap((components) => { + return this.resolveRuleNodeComponentsUiResources(components).pipe( + map((ruleNodeComponents) => { + this.ruleNodeComponents = ruleNodeComponents; + this.ruleNodeComponents.push(ruleChainNodeComponent); + this.ruleNodeComponents.sort( + (comp1, comp2) => { + let result = comp1.type.toString().localeCompare(comp2.type.toString()); + if (result === 0) { + result = comp1.name.localeCompare(comp2.name); + } + return result; + } + ); + return this.ruleNodeComponents; + }) + ); + }) + ); + } + } + + public getRuleNodeComponentByClazz(clazz: string): RuleNodeComponentDescriptor { + const found = this.ruleNodeComponents.filter((component) => component.clazz === clazz); + if (found && found.length) { + return found[0]; + } else { + const unknownComponent = deepClone(unknownNodeComponent); + unknownComponent.clazz = clazz; + unknownComponent.configurationDescriptor.nodeDefinition.details = 'Unknown Rule Node class: ' + clazz; + return unknownComponent; + } + } + + private resolveTargetRuleChains(ruleChainConnections: Array): Observable<{[ruleChainId: string]: RuleChain}> { + if (ruleChainConnections && ruleChainConnections.length) { + const tasks: Observable[] = []; + ruleChainConnections.forEach((connection) => { + tasks.push(this.resolveRuleChain(connection.targetRuleChainId.id)); + }); + return forkJoin(tasks).pipe( + map((ruleChains) => { + const ruleChainsMap: {[ruleChainId: string]: RuleChain} = {}; + ruleChains.forEach((ruleChain) => { + ruleChainsMap[ruleChain.id.id] = ruleChain; + }); + return ruleChainsMap; + }) + ); + } else { + return of({} as {[ruleChainId: string]: RuleChain}); + } + } + + private loadRuleNodeComponents(config?: RequestConfig): Observable> { + return this.componentDescriptorService.getComponentDescriptorsByTypes(ruleNodeTypeComponentTypes, config).pipe( + map((components) => { + const ruleNodeComponents: RuleNodeComponentDescriptor[] = []; + components.forEach((component) => { + ruleNodeComponents.push(component as RuleNodeComponentDescriptor); + }); + return ruleNodeComponents; + }) + ); + } + + private resolveRuleNodeComponentsUiResources(components: Array): + Observable> { + const tasks: Observable[] = []; + components.forEach((component) => { + tasks.push(this.resolveRuleNodeComponentUiResources(component)); + }); + return forkJoin(tasks).pipe( + catchError((err) => { + return of(components); + }) + ); + } + + private resolveRuleNodeComponentUiResources(component: RuleNodeComponentDescriptor): Observable { + const uiResources = component.configurationDescriptor.nodeDefinition.uiResources; + if (uiResources && uiResources.length) { + const tasks: Observable[] = []; + uiResources.forEach((uiResource) => { + tasks.push(this.resourcesService.loadResource(uiResource)); + }); + return forkJoin(tasks).pipe( + map((res) => { + return component; + }), + catchError(() => { + component.configurationDescriptor.nodeDefinition.uiResourceLoadError = this.translate.instant('rulenode.ui-resources-load-error'); + return of(component); + }) + ); + } else { + return of(component); + } + } + + private resolveRuleChain(ruleChainId: string): Observable { + return this.getRuleChain(ruleChainId, {ignoreErrors: true}).pipe( + map(ruleChain => ruleChain), + catchError((err) => { + const ruleChain = { + id: { + entityType: EntityType.RULE_CHAIN, + id: ruleChainId + } + } as RuleChain; + return of(ruleChain); + }) + ); + } + } 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 1cdb167ddf..827115dc38 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,9 +17,9 @@ --> 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 a604312074..7f073551c0 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 @@ -14,160 +14,341 @@ /// limitations under the License. /// -import {Component, OnInit} from '@angular/core'; -import {UserService} from '@core/http/user.service'; -import {User} from '@shared/models/user.model'; -import {Authority} from '@shared/models/authority.enum'; -import {PageComponent} from '@shared/components/page.component'; -import {Store} from '@ngrx/store'; -import {AppState} from '@core/core.state'; -import {FormBuilder, FormGroup, Validators} from '@angular/forms'; -import {HasConfirmForm} from '@core/guards/confirm-on-exit.guard'; -import {ActionAuthUpdateUserDetails} from '@core/auth/auth.actions'; -import {environment as env} from '@env/environment'; -import {TranslateService} from '@ngx-translate/core'; -import {ActionSettingsChangeLanguage} from '@core/settings/settings.actions'; -import {ChangePasswordDialogComponent} from '@modules/home/pages/profile/change-password-dialog.component'; -import {MatDialog} from '@angular/material'; -import {DialogService} from '@core/services/dialog.service'; -import {AuthService} from '@core/auth/auth.service'; -import {ActivatedRoute} from '@angular/router'; -import { Dashboard } from '@shared/models/dashboard.models'; -import { RuleChain } from '@shared/models/rule-chain.models'; -import { FcModel, FlowchartConstants } from 'ngx-flowchart/dist/ngx-flowchart'; +import { Component, OnInit } 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 { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material'; +import { DialogService } from '@core/services/dialog.service'; +import { AuthService } from '@core/auth/auth.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { + inputNodeComponent, + ResolvedRuleChainMetaData, + RuleChain, + ruleChainNodeComponent +} from '@shared/models/rule-chain.models'; +import { FlowchartConstants } from 'ngx-flowchart/dist/ngx-flowchart'; +import { RuleNodeComponentDescriptor, RuleNodeType, ruleNodeTypeDescriptors } from '@shared/models/rule-node.models'; +import { FcRuleEdge, FcRuleNode, FcRuleNodeModel, FcRuleNodeType, FcRuleNodeTypeModel } from './rulechain-page.models'; +import { RuleChainService } from '@core/http/rule-chain.service'; @Component({ selector: 'tb-rulechain-page', templateUrl: './rulechain-page.component.html', styleUrls: [] }) -export class RuleChainPageComponent extends PageComponent implements OnInit { +export class RuleChainPageComponent extends PageComponent implements OnInit, HasDirtyFlag { + + get isDirty(): boolean { + return this.isDirtyValue || this.isImport; + } + + isImport: boolean; + isDirtyValue: boolean; + + errorTooltips = {}; + isFullscreen = false; + + editingRuleNode = null; + isEditingRuleNode = false; + + editingRuleNodeLink = null; + isEditingRuleNodeLink = false; + + isLibraryOpen = true; + + ruleNodeSearch = ''; ruleChain: RuleChain; + ruleChainMetaData: ResolvedRuleChainMetaData; - flowchartConstants = FlowchartConstants; - selected = []; - model: FcModel = { + ruleChainModel: FcRuleNodeModel = { nodes: [], edges: [] }; + selectedObjects = []; + nextNodeID: number; + nextConnectorID: number; + inputConnectorId: number; + + ruleNodeTypesModel: {[type: string]: {model: FcRuleNodeTypeModel, selectedObjects: any[]}} = {}; + + ruleNodeComponents: Array; + + flowchartConstants = FlowchartConstants; constructor(protected store: Store, private route: ActivatedRoute, - private userService: UserService, + private router: Router, + private ruleChainService: RuleChainService, private authService: AuthService, private translate: TranslateService, public dialog: MatDialog, public dialogService: DialogService, public fb: FormBuilder) { super(store); + this.init(); } ngOnInit() { + } + + private init() { this.ruleChain = this.route.snapshot.data.ruleChain; - this.testData(); + if (this.route.snapshot.data.import && !this.ruleChain) { + this.router.navigateByUrl('ruleChains'); + return; + } + this.isImport = this.route.snapshot.data.import; + this.ruleChainMetaData = this.route.snapshot.data.ruleChainMetaData; + this.ruleNodeComponents = this.route.snapshot.data.ruleNodeComponents; + for (const type of Object.keys(RuleNodeType)) { + const desc = ruleNodeTypeDescriptors.get(RuleNodeType[type]); + if (!desc.special) { + this.ruleNodeTypesModel[type] = { + model: { + nodes: [], + edges: [] + }, + selectedObjects: [] + }; + } + } + this.loadRuleChainLibrary(this.ruleNodeComponents); + this.createRuleChainModel(); } - onModelChanged() { - console.log('Model changed!'); + private loadRuleChainLibrary(ruleNodeComponents: Array) { + for (const componentType of Object.keys(this.ruleNodeTypesModel)) { + this.ruleNodeTypesModel[componentType].model.nodes.length = 0; + } + ruleNodeComponents.forEach((ruleNodeComponent) => { + const componentType = ruleNodeComponent.type; + const model = this.ruleNodeTypesModel[componentType].model; + const desc = ruleNodeTypeDescriptors.get(RuleNodeType[componentType]); + let icon = desc.icon; + let iconUrl = null; + if (ruleNodeComponent.configurationDescriptor.nodeDefinition.icon) { + icon = ruleNodeComponent.configurationDescriptor.nodeDefinition.icon; + } + if (ruleNodeComponent.configurationDescriptor.nodeDefinition.iconUrl) { + iconUrl = ruleNodeComponent.configurationDescriptor.nodeDefinition.iconUrl; + } + const node: FcRuleNodeType = { + id: 'node-lib-' + componentType + '-' + model.nodes.length, + component: ruleNodeComponent, + name: '', + nodeClass: desc.nodeClass, + icon, + iconUrl, + x: 30, + y: 10 + 50 * model.nodes.length, + connectors: [] + }; + if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) { + node.connectors.push( + { + type: FlowchartConstants.leftConnectorType, + id: (model.nodes.length * 2) + '' + } + ); + } + if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) { + node.connectors.push( + { + type: FlowchartConstants.rightConnectorType, + id: (model.nodes.length * 2 + 1) + '' + } + ); + } + model.nodes.push(node); + }); } - private testData() { - this.model.nodes.push(... - [ - { - name: 'Node 1', - readonly: true, - id: '2', - x: 300, - y: 100, - color: '#000', - borderColor: '#000', - connectors: [ - { - type: FlowchartConstants.leftConnectorType, - id: '1' - }, - { - type: FlowchartConstants.rightConnectorType, - id: '2' - } - ] - }, - { - name: 'Node 2', - id: '3', - x: 600, - y: 100, - color: '#F15B26', - connectors: [ - { - type: FlowchartConstants.leftConnectorType, - id: '3' - }, - { - type: FlowchartConstants.rightConnectorType, - id: '4' - } - ] - }, - { - name: 'Node 3', - id: '4', - x: 1000, - y: 100, - color: '#000', - borderColor: '#000', - connectors: [ - { - type: FlowchartConstants.leftConnectorType, - id: '5' - }, - { - type: FlowchartConstants.rightConnectorType, - id: '6' - } - ] - }, - { - name: 'Node 4', - id: '5', - x: 1300, - y: 100, - color: '#000', - borderColor: '#000', - connectors: [ - { - type: FlowchartConstants.leftConnectorType, - id: '7' - }, - { - type: FlowchartConstants.rightConnectorType, - id: '8' + private createRuleChainModel() { + this.nextNodeID = 1; + this.nextConnectorID = 1; + + this.selectedObjects.length = 0; + this.ruleChainModel.nodes.length = 0; + this.ruleChainModel.edges.length = 0; + + this.inputConnectorId = this.nextConnectorID++; + this.ruleChainModel.nodes.push( + { + id: 'rule-chain-node-' + this.nextNodeID++, + component: inputNodeComponent, + name: '', + nodeClass: ruleNodeTypeDescriptors.get(RuleNodeType.INPUT).nodeClass, + icon: ruleNodeTypeDescriptors.get(RuleNodeType.INPUT).icon, + readonly: true, + x: 50, + y: 150, + connectors: [ + { + type: FlowchartConstants.rightConnectorType, + id: this.inputConnectorId + '' + }, + ] + + } + ); + const nodes: FcRuleNode[] = []; + this.ruleChainMetaData.nodes.forEach((ruleNode) => { + const component = this.ruleChainService.getRuleNodeComponentByClazz(ruleNode.type); + const descriptor = ruleNodeTypeDescriptors.get(component.type); + let icon = descriptor.icon; + let iconUrl = null; + if (component.configurationDescriptor.nodeDefinition.icon) { + icon = component.configurationDescriptor.nodeDefinition.icon; + } + if (component.configurationDescriptor.nodeDefinition.iconUrl) { + iconUrl = component.configurationDescriptor.nodeDefinition.iconUrl; + } + const node: FcRuleNode = { + id: 'rule-chain-node-' + this.nextNodeID++, + ruleNodeId: ruleNode.id, + additionalInfo: ruleNode.additionalInfo, + configuration: ruleNode.configuration, + debugMode: ruleNode.debugMode, + x: Math.round(ruleNode.additionalInfo.layoutX), + y: Math.round(ruleNode.additionalInfo.layoutY), + component, + name: ruleNode.name, + nodeClass: descriptor.nodeClass, + icon, + iconUrl, + connectors: [] + }; + if (component.configurationDescriptor.nodeDefinition.inEnabled) { + node.connectors.push( + { + type: FlowchartConstants.leftConnectorType, + id: (this.nextConnectorID++) + '' + } + ); + } + if (component.configurationDescriptor.nodeDefinition.outEnabled) { + node.connectors.push( + { + type: FlowchartConstants.rightConnectorType, + id: (this.nextConnectorID++) + '' + } + ); + } + nodes.push(node); + this.ruleChainModel.nodes.push(node); + }); + if (this.ruleChainMetaData.firstNodeIndex > -1) { + const destNode = nodes[this.ruleChainMetaData.firstNodeIndex]; + if (destNode) { + const connectors = destNode.connectors.filter(connector => connector.type === FlowchartConstants.leftConnectorType); + if (connectors && connectors.length) { + const edge: FcRuleEdge = { + source: this.inputConnectorId + '', + destination: connectors[0].id + }; + this.ruleChainModel.edges.push(edge); + } + } + } + if (this.ruleChainMetaData.connections) { + const edgeMap: {[edgeKey: string]: FcRuleEdge} = {}; + this.ruleChainMetaData.connections.forEach((connection) => { + const sourceNode = nodes[connection.fromIndex]; + const destNode = nodes[connection.toIndex]; + if (sourceNode && destNode) { + const sourceConnectors = sourceNode.connectors.filter(connector => connector.type === FlowchartConstants.rightConnectorType); + const destConnectors = destNode.connectors.filter(connector => connector.type === FlowchartConstants.leftConnectorType); + if (sourceConnectors && sourceConnectors.length && destConnectors && destConnectors.length) { + const sourceId = sourceConnectors[0].id; + const destId = destConnectors[0].id; + const edgeKey = sourceId + '_' + destId; + let edge = edgeMap[edgeKey]; + if (!edge) { + edge = { + source: sourceId, + destination: destId, + label: connection.type, + labels: [connection.type] + }; + edgeMap[edgeKey] = edge; + this.ruleChainModel.edges.push(edge); + } else { + edge.label += ' / ' + connection.type; + edge.labels.push(connection.type); } - ] + } } - ] - ); - this.model.edges.push(... - [ - { - source: '2', - destination: '3', - label: 'label1' - }, - { - source: '4', - destination: '5', - label: 'label2' - }, - { - source: '6', - destination: '7', - label: 'label3' + }); + } + if (this.ruleChainMetaData.ruleChainConnections) { + const ruleChainsMap = this.ruleChainMetaData.targetRuleChainsMap; + const ruleChainNodesMap: {[ruleChainNodeId: string]: FcRuleNode} = {}; + const ruleChainEdgeMap: {[edgeKey: string]: FcRuleEdge} = {}; + this.ruleChainMetaData.ruleChainConnections.forEach((ruleChainConnection) => { + const ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id]; + if (ruleChainConnection.additionalInfo && ruleChainConnection.additionalInfo.ruleChainNodeId) { + let ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId]; + if (!ruleChainNode) { + ruleChainNode = { + id: 'rule-chain-node-' + this.nextNodeID++, + name: ruleChain.name ? name : 'Unresolved', + targetRuleChainId: ruleChain.name ? ruleChainConnection.targetRuleChainId.id : null, + error: ruleChain.name ? undefined : this.translate.instant('rulenode.invalid-target-rulechain'), + additionalInfo: ruleChainConnection.additionalInfo, + x: Math.round(ruleChainConnection.additionalInfo.layoutX), + y: Math.round(ruleChainConnection.additionalInfo.layoutY), + component: ruleChainNodeComponent, + nodeClass: ruleNodeTypeDescriptors.get(RuleNodeType.RULE_CHAIN).nodeClass, + icon: ruleNodeTypeDescriptors.get(RuleNodeType.RULE_CHAIN).icon, + connectors: [ + { + type: FlowchartConstants.leftConnectorType, + id: (this.nextConnectorID++) + '' + } + ] + }; + ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode; + this.ruleChainModel.nodes.push(ruleChainNode); + } + const sourceNode = nodes[ruleChainConnection.fromIndex]; + if (sourceNode) { + const connectors = sourceNode.connectors.filter(connector => connector.type === FlowchartConstants.rightConnectorType); + if (connectors && connectors.length) { + const sourceId = connectors[0].id; + const destId = ruleChainNode.connectors[0].id; + const edgeKey = sourceId + '_' + destId; + let ruleChainEdge = ruleChainEdgeMap[edgeKey]; + if (!ruleChainEdge) { + ruleChainEdge = { + source: sourceId, + destination: destId, + label: ruleChainConnection.type, + labels: [ruleChainConnection.type] + }; + ruleChainEdgeMap[edgeKey] = ruleChainEdge; + this.ruleChainModel.edges.push(ruleChainEdge); + } else { + ruleChainEdge.label += ' / ' + ruleChainConnection.type; + ruleChainEdge.labels.push(ruleChainConnection.type); + } + } + } } - ] - ); + }); + } + this.isDirtyValue = false; + } + + onModelChanged() { + console.log('Model changed!'); + this.isDirtyValue = true; } + } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.models.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.models.ts new file mode 100644 index 0000000000..c7e972625b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.models.ts @@ -0,0 +1,49 @@ +/// +/// 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 { FcNode, FcEdge, FcModel } from 'ngx-flowchart/dist/ngx-flowchart'; +import { RuleNodeComponentDescriptor, RuleNodeConfiguration } from '@shared/models/rule-node.models'; +import { RuleNodeId } from '@app/shared/models/id/rule-node-id'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; + +export interface FcRuleNodeType extends FcNode { + component: RuleNodeComponentDescriptor; + nodeClass: string; + icon: string; + iconUrl?: string; +} + +export interface FcRuleNodeTypeModel extends FcModel { + nodes: Array; +} + +export interface FcRuleNode extends FcRuleNodeType { + ruleNodeId?: RuleNodeId; + additionalInfo?: any; + configuration?: RuleNodeConfiguration; + debugMode?: boolean; + targetRuleChainId?: string; + error?: string; +} + +export interface FcRuleEdge extends FcEdge { + labels?: string[]; +} + +export interface FcRuleNodeModel extends FcModel { + nodes: Array; + edges: Array; +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts index 129da15ff1..93f25ecc7f 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts @@ -26,11 +26,18 @@ import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; -import { RuleChain } from '@shared/models/rule-chain.models'; +import { + RuleChain, + RuleChainMetaData, + RuleChainImport, + ResolvedRuleChainMetaData +} from '@shared/models/rule-chain.models'; import { RuleChainService } from '@core/http/rule-chain.service'; import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component'; import { dashboardBreadcumbLabelFunction, DashboardResolver } from '@home/pages/dashboard/dashboard-routing.module'; import { RuleChainPageComponent } from '@home/pages/rulechain/rulechain-page.component'; +import { RuleNodeComponentDescriptor } from '@shared/models/rule-node.models'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; @Injectable() @@ -45,6 +52,29 @@ export class RuleChainResolver implements Resolve { } } +@Injectable() +export class ResolvedRuleChainMetaDataResolver implements Resolve { + + constructor(private ruleChainService: RuleChainService) { + } + + resolve(route: ActivatedRouteSnapshot): Observable { + const ruleChainId = route.params.ruleChainId; + return this.ruleChainService.getResolvedRuleChainMetadata(ruleChainId); + } +} + +@Injectable() +export class RuleNodeComponentsResolver implements Resolve> { + + constructor(private ruleChainService: RuleChainService) { + } + + resolve(route: ActivatedRouteSnapshot): Observable> { + return this.ruleChainService.getRuleNodeComponents(); + } +} + export const ruleChainBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate, component) => { let label: string = component.ruleChain.name; if (component.ruleChain.root) { @@ -53,6 +83,10 @@ export const ruleChainBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, return label; }); +export const importRuleChainBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate, component) => { + return `${translate.instant('rulechain.import')}: ${component.ruleChain.name}`; +}); + const routes: Routes = [ { path: 'ruleChains', @@ -77,6 +111,7 @@ const routes: Routes = [ { path: ':ruleChainId', component: RuleChainPageComponent, + canDeactivate: [ConfirmOnExitGuard], data: { breadcrumb: { labelFunction: ruleChainBreadcumbLabelFunction, @@ -84,10 +119,31 @@ const routes: Routes = [ } as BreadCrumbConfig, auth: [Authority.TENANT_ADMIN], title: 'rulechain.rulechain', - widgetEditMode: false + import: false }, resolve: { - ruleChain: RuleChainResolver + ruleChain: RuleChainResolver, + ruleChainMetaData: ResolvedRuleChainMetaDataResolver, + ruleNodeComponents: RuleNodeComponentsResolver + } + }, + { + path: 'ruleChain/import', + component: RuleChainPageComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: importRuleChainBreadcumbLabelFunction, + icon: 'settings_ethernet' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN], + title: 'rulechain.rulechain', + import: true + }, + resolve: { + ruleChain: 'importRuleChain', + ruleChainMetaData: 'importRuleChainMetadata', + ruleNodeComponents: RuleNodeComponentsResolver } } ] @@ -99,7 +155,23 @@ const routes: Routes = [ exports: [RouterModule], providers: [ RuleChainsTableConfigResolver, - RuleChainResolver + RuleChainResolver, + ResolvedRuleChainMetaDataResolver, + RuleNodeComponentsResolver, + { + provide: 'importRuleChain', + useValue: (route: ActivatedRouteSnapshot) => { + const ruleChainImport: RuleChainImport = route.params.ruleChainImport; + return ruleChainImport.ruleChain; + } + }, + { + provide: 'importRuleChainMetadata', + useValue: (route: ActivatedRouteSnapshot) => { + const ruleChainImport: RuleChainImport = route.params.ruleChainImport; + return ruleChainImport.metadata; + } + } ] }) export class RuleChainRoutingModule { } diff --git a/ui-ngx/src/app/shared/models/component-descriptor.models.ts b/ui-ngx/src/app/shared/models/component-descriptor.models.ts new file mode 100644 index 0000000000..47f81f96b6 --- /dev/null +++ b/ui-ngx/src/app/shared/models/component-descriptor.models.ts @@ -0,0 +1,39 @@ +/// +/// 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 { RuleNodeType } from '@shared/models/rule-node.models'; + +export enum ComponentType { + ENRICHMENT = 'ENRICHMENT', + FILTER = 'FILTER', + TRANSFORMATION = 'TRANSFORMATION', + ACTION = 'ACTION', + EXTERNAL = 'EXTERNAL' +} + +export enum ComponentScope { + SYSTEM = 'SYSTEM', + TENANT = 'TENANT' +} + +export interface ComponentDescriptor { + type: ComponentType | RuleNodeType; + scope?: ComponentScope; + name: string; + clazz: string; + configurationDescriptor?: any; + actions?: string; +} diff --git a/ui-ngx/src/app/shared/models/rule-chain.models.ts b/ui-ngx/src/app/shared/models/rule-chain.models.ts index 100214a8cc..3f6c205e08 100644 --- a/ui-ngx/src/app/shared/models/rule-chain.models.ts +++ b/ui-ngx/src/app/shared/models/rule-chain.models.ts @@ -14,10 +14,12 @@ /// limitations under the License. /// -import {BaseData} from '@shared/models/base-data'; -import {TenantId} from '@shared/models/id/tenant-id'; -import {RuleChainId} from '@shared/models/id/rule-chain-id'; -import {RuleNodeId} from '@shared/models/id/rule-node-id'; +import { BaseData } from '@shared/models/base-data'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; +import { RuleNodeId } from '@shared/models/id/rule-node-id'; +import { RuleNode, RuleNodeComponentDescriptor, RuleNodeType } from '@shared/models/rule-node.models'; +import { ComponentType } from '@shared/models/component-descriptor.models'; export interface RuleChain extends BaseData { tenantId: TenantId; @@ -28,3 +30,82 @@ export interface RuleChain extends BaseData { configuration?: any; additionalInfo?: any; } + +export interface RuleChainMetaData { + ruleChainId: RuleChainId; + firstNodeIndex: number; + nodes: Array; + connections: Array; + ruleChainConnections: Array; +} + +export interface ResolvedRuleChainMetaData extends RuleChainMetaData { + targetRuleChainsMap: {[ruleChainId: string]: RuleChain}; +} + +export interface RuleChainImport { + ruleChain: RuleChain; + metadata: ResolvedRuleChainMetaData; +} + +export interface NodeConnectionInfo { + fromIndex: number; + toIndex: number; + type: string; +} + +export interface RuleChainConnectionInfo { + fromIndex: number; + targetRuleChainId: RuleChainId; + additionalInfo: any; + type: string; +} + +export const ruleNodeTypeComponentTypes: ComponentType[] = + [ + ComponentType.FILTER, + ComponentType.ENRICHMENT, + ComponentType.TRANSFORMATION, + ComponentType.ACTION, + ComponentType.EXTERNAL + ]; + +export const ruleChainNodeComponent: RuleNodeComponentDescriptor = { + type: RuleNodeType.RULE_CHAIN, + name: 'rule chain', + clazz: 'tb.internal.RuleChain', + configurationDescriptor: { + nodeDefinition: { + description: '', + details: 'Forwards incoming messages to specified Rule Chain', + inEnabled: true, + outEnabled: false, + relationTypes: [], + customRelations: false, + defaultConfiguration: {} + } + } +}; + +export const unknownNodeComponent: RuleNodeComponentDescriptor = { + type: RuleNodeType.UNKNOWN, + name: 'unknown', + clazz: 'tb.internal.Unknown', + configurationDescriptor: { + nodeDefinition: { + description: '', + details: '', + inEnabled: true, + outEnabled: true, + relationTypes: [], + customRelations: false, + defaultConfiguration: {} + } + } +}; + +export const inputNodeComponent: RuleNodeComponentDescriptor = { + type: RuleNodeType.INPUT, + name: 'Input', + clazz: 'tb.internal.Input' +}; 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 360c8099dc..fa75276e96 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -20,6 +20,8 @@ import {TenantId} from '@shared/models/id/tenant-id'; import {CustomerId} from '@shared/models/id/customer-id'; import {RuleChainId} from '@shared/models/id/rule-chain-id'; import {RuleNodeId} from '@shared/models/id/rule-node-id'; +import { ComponentDescriptor, ComponentType } from '@shared/models/component-descriptor.models'; +import { EntityType, EntityTypeResource } from '@shared/models/entity-type.models'; export enum MsgDataType { JSON = 'JSON', @@ -28,7 +30,7 @@ export enum MsgDataType { } export interface RuleNodeConfiguration { - todo: Array; + [key: string]: any; // TODO: } @@ -40,3 +42,130 @@ export interface RuleNode extends BaseData { configuration: RuleNodeConfiguration; additionalInfo?: any; } + +export interface RuleNodeConfigurationDescriptor { + nodeDefinition: { + description: string; + details: string; + inEnabled: boolean; + outEnabled: boolean; + relationTypes: string[]; + customRelations: boolean; + defaultConfiguration: any; + icon?: string; + iconUrl?: string; + uiResources?: string[]; + uiResourceLoadError?: string; + }; +} + +export enum RuleNodeType { + FILTER = 'FILTER', + ENRICHMENT = 'ENRICHMENT', + TRANSFORMATION = 'TRANSFORMATION', + ACTION = 'ACTION', + EXTERNAL = 'EXTERNAL', + RULE_CHAIN = 'RULE_CHAIN', + UNKNOWN = 'UNKNOWN', + INPUT = 'INPUT' +} + +export interface RuleNodeTypeDescriptor { + value: RuleNodeType; + name: string; + details: string; + nodeClass: string; + icon: string; + special?: boolean; +} + +export const ruleNodeTypeDescriptors = new Map( + [ + [ + RuleNodeType.FILTER, + { + value: RuleNodeType.FILTER, + name: 'rulenode.type-filter', + details: 'rulenode.type-filter-details', + nodeClass: 'tb-filter-type', + icon: 'filter_list' + } + ], + [ + RuleNodeType.ENRICHMENT, + { + value: RuleNodeType.ENRICHMENT, + name: 'rulenode.type-enrichment', + details: 'rulenode.type-enrichment-details', + nodeClass: 'tb-enrichment-type', + icon: 'playlist_add' + } + ], + [ + RuleNodeType.TRANSFORMATION, + { + value: RuleNodeType.TRANSFORMATION, + name: 'rulenode.type-transformation', + details: 'rulenode.type-transformation-details', + nodeClass: 'tb-transformation-type', + icon: 'transform' + } + ], + [ + RuleNodeType.ACTION, + { + value: RuleNodeType.ACTION, + name: 'rulenode.type-action', + details: 'rulenode.type-action-details', + nodeClass: 'tb-action-type', + icon: 'flash_on' + } + ], + [ + RuleNodeType.EXTERNAL, + { + value: RuleNodeType.EXTERNAL, + name: 'rulenode.type-external', + details: 'rulenode.type-external-details', + nodeClass: 'tb-external-type', + icon: 'cloud_upload' + } + ], + [ + RuleNodeType.RULE_CHAIN, + { + value: RuleNodeType.RULE_CHAIN, + name: 'rulenode.type-rule-chain', + details: 'rulenode.type-rule-chain-details', + nodeClass: 'tb-rule-chain-type', + icon: 'settings_ethernet' + } + ], + [ + RuleNodeType.INPUT, + { + value: RuleNodeType.INPUT, + name: 'rulenode.type-input', + details: 'rulenode.type-input-details', + nodeClass: 'tb-input-type', + icon: 'input', + special: true + } + ], + [ + RuleNodeType.UNKNOWN, + { + value: RuleNodeType.UNKNOWN, + name: 'rulenode.type-unknown', + details: 'rulenode.type-unknown-details', + nodeClass: 'tb-unknown-type', + icon: 'help_outline' + } + ] + ] +); + +export interface RuleNodeComponentDescriptor extends ComponentDescriptor { + type: RuleNodeType; + configurationDescriptor?: RuleNodeConfigurationDescriptor; +}