Browse Source

RuleChain page implementation.

pull/2411/head
Igor Kulikov 7 years ago
parent
commit
cabdc68a42
  1. 4
      ui-ngx/proxy.conf.json
  2. 103
      ui-ngx/src/app/core/http/component-descriptor.service.ts
  3. 189
      ui-ngx/src/app/core/http/rule-chain.service.ts
  4. 4
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html
  5. 425
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  6. 49
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.models.ts
  7. 80
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts
  8. 39
      ui-ngx/src/app/shared/models/component-descriptor.models.ts
  9. 89
      ui-ngx/src/app/shared/models/rule-chain.models.ts
  10. 131
      ui-ngx/src/app/shared/models/rule-node.models.ts

4
ui-ngx/proxy.conf.json

@ -3,6 +3,10 @@
"target": "http://localhost:8080", "target": "http://localhost:8080",
"secure": false "secure": false
}, },
"/static/rulenode": {
"target": "http://localhost:8080",
"secure": false
},
"/api/ws": { "/api/ws": {
"target": "ws://localhost:8080", "target": "ws://localhost:8080",
"ws": true "ws": true

103
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<ComponentType | RuleNodeType, Array<ComponentDescriptor>> =
new Map<ComponentType, Array<ComponentDescriptor>>();
private componentsByClazz: Map<string, ComponentDescriptor> = new Map<string, ComponentDescriptor>();
constructor(
private http: HttpClient
) {
}
public getComponentDescriptorsByType(componentType: ComponentType, config?: RequestConfig): Observable<Array<ComponentDescriptor>> {
const existing = this.componentsByType.get(componentType);
if (existing) {
return of(existing);
} else {
return this.http.get<Array<ComponentDescriptor>>(`/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<ComponentType>,
config?: RequestConfig): Observable<Array<ComponentDescriptor>> {
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<Array<ComponentDescriptor>>(`/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<ComponentDescriptor>();
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<ComponentDescriptor> {
const existing = this.componentsByClazz.get(componentDescriptorClazz);
if (existing) {
return of(existing);
} else {
return this.http.get<ComponentDescriptor>(`/api/component/${componentDescriptorClazz}`, defaultHttpOptionsFromConfig(config)).pipe(
map((componentDescriptor) => {
this.componentsByClazz.set(componentDescriptorClazz, componentDescriptor);
return componentDescriptor;
})
);
}
}
}

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

@ -14,21 +14,39 @@
/// limitations under the License. /// limitations under the License.
/// ///
import {Injectable} from '@angular/core'; import { Injectable } from '@angular/core';
import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import {Observable} from 'rxjs/index'; import { forkJoin, Observable, of } from 'rxjs/index';
import {HttpClient} from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import {PageLink} from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import {PageData} from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import {RuleChain} from '@shared/models/rule-chain.models'; 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class RuleChainService { export class RuleChainService {
private ruleNodeComponents: Array<RuleNodeComponentDescriptor>;
constructor( constructor(
private http: HttpClient private http: HttpClient,
private componentDescriptorService: ComponentDescriptorService,
private resourcesService: ResourcesService,
private translate: TranslateService
) { } ) { }
public getRuleChains(pageLink: PageLink, config?: RequestConfig): Observable<PageData<RuleChain>> { public getRuleChains(pageLink: PageLink, config?: RequestConfig): Observable<PageData<RuleChain>> {
@ -52,4 +70,159 @@ export class RuleChainService {
return this.http.post<RuleChain>(`/api/ruleChain/${ruleChainId}/root`, null, defaultHttpOptionsFromConfig(config)); return this.http.post<RuleChain>(`/api/ruleChain/${ruleChainId}/root`, null, defaultHttpOptionsFromConfig(config));
} }
public getRuleChainMetadata(ruleChainId: string, config?: RequestConfig): Observable<RuleChainMetaData> {
return this.http.get<RuleChainMetaData>(`/api/ruleChain/${ruleChainId}/metadata`, defaultHttpOptionsFromConfig(config));
}
public getResolvedRuleChainMetadata(ruleChainId: string, config?: RequestConfig): Observable<ResolvedRuleChainMetaData> {
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<RuleChainMetaData> {
return this.http.post<RuleChainMetaData>('/api/ruleChain/metadata', ruleChainMetaData, defaultHttpOptionsFromConfig(config));
}
public saveAndGetResolvedRuleChainMetadata(ruleChainMetaData: RuleChainMetaData,
config?: RequestConfig): Observable<ResolvedRuleChainMetaData> {
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<Array<RuleNodeComponentDescriptor>> {
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<RuleChainConnectionInfo>): Observable<{[ruleChainId: string]: RuleChain}> {
if (ruleChainConnections && ruleChainConnections.length) {
const tasks: Observable<RuleChain>[] = [];
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<Array<RuleNodeComponentDescriptor>> {
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<RuleNodeComponentDescriptor>):
Observable<Array<RuleNodeComponentDescriptor>> {
const tasks: Observable<RuleNodeComponentDescriptor>[] = [];
components.forEach((component) => {
tasks.push(this.resolveRuleNodeComponentUiResources(component));
});
return forkJoin(tasks).pipe(
catchError((err) => {
return of(components);
})
);
}
private resolveRuleNodeComponentUiResources(component: RuleNodeComponentDescriptor): Observable<RuleNodeComponentDescriptor> {
const uiResources = component.configurationDescriptor.nodeDefinition.uiResources;
if (uiResources && uiResources.length) {
const tasks: Observable<any>[] = [];
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<RuleChain> {
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);
})
);
}
} }

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

@ -17,9 +17,9 @@
--> -->
<fc-canvas #fcCanvas <fc-canvas #fcCanvas
id="fc-target-canvas" id="fc-target-canvas"
[model]="model" [model]="ruleChainModel"
(modelChanged)="onModelChanged()" (modelChanged)="onModelChanged()"
[selectedObjects]="selected" [selectedObjects]="selectedObjects"
[edgeStyle]="flowchartConstants.curvedStyle" [edgeStyle]="flowchartConstants.curvedStyle"
[automaticResize]="true" [automaticResize]="true"
[dragAnimation]="flowchartConstants.dragAnimationRepaint"> [dragAnimation]="flowchartConstants.dragAnimationRepaint">

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

@ -14,160 +14,341 @@
/// limitations under the License. /// limitations under the License.
/// ///
import {Component, OnInit} from '@angular/core'; import { Component, OnInit } from '@angular/core';
import {UserService} from '@core/http/user.service'; import { PageComponent } from '@shared/components/page.component';
import {User} from '@shared/models/user.model'; import { Store } from '@ngrx/store';
import {Authority} from '@shared/models/authority.enum'; import { AppState } from '@core/core.state';
import {PageComponent} from '@shared/components/page.component'; import { FormBuilder } from '@angular/forms';
import {Store} from '@ngrx/store'; import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
import {AppState} from '@core/core.state'; import { TranslateService } from '@ngx-translate/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms'; import { MatDialog } from '@angular/material';
import {HasConfirmForm} from '@core/guards/confirm-on-exit.guard'; import { DialogService } from '@core/services/dialog.service';
import {ActionAuthUpdateUserDetails} from '@core/auth/auth.actions'; import { AuthService } from '@core/auth/auth.service';
import {environment as env} from '@env/environment'; import { ActivatedRoute, Router } from '@angular/router';
import {TranslateService} from '@ngx-translate/core'; import {
import {ActionSettingsChangeLanguage} from '@core/settings/settings.actions'; inputNodeComponent,
import {ChangePasswordDialogComponent} from '@modules/home/pages/profile/change-password-dialog.component'; ResolvedRuleChainMetaData,
import {MatDialog} from '@angular/material'; RuleChain,
import {DialogService} from '@core/services/dialog.service'; ruleChainNodeComponent
import {AuthService} from '@core/auth/auth.service'; } from '@shared/models/rule-chain.models';
import {ActivatedRoute} from '@angular/router'; import { FlowchartConstants } from 'ngx-flowchart/dist/ngx-flowchart';
import { Dashboard } from '@shared/models/dashboard.models'; import { RuleNodeComponentDescriptor, RuleNodeType, ruleNodeTypeDescriptors } from '@shared/models/rule-node.models';
import { RuleChain } from '@shared/models/rule-chain.models'; import { FcRuleEdge, FcRuleNode, FcRuleNodeModel, FcRuleNodeType, FcRuleNodeTypeModel } from './rulechain-page.models';
import { FcModel, FlowchartConstants } from 'ngx-flowchart/dist/ngx-flowchart'; import { RuleChainService } from '@core/http/rule-chain.service';
@Component({ @Component({
selector: 'tb-rulechain-page', selector: 'tb-rulechain-page',
templateUrl: './rulechain-page.component.html', templateUrl: './rulechain-page.component.html',
styleUrls: [] 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; ruleChain: RuleChain;
ruleChainMetaData: ResolvedRuleChainMetaData;
flowchartConstants = FlowchartConstants; ruleChainModel: FcRuleNodeModel = {
selected = [];
model: FcModel = {
nodes: [], nodes: [],
edges: [] edges: []
}; };
selectedObjects = [];
nextNodeID: number;
nextConnectorID: number;
inputConnectorId: number;
ruleNodeTypesModel: {[type: string]: {model: FcRuleNodeTypeModel, selectedObjects: any[]}} = {};
ruleNodeComponents: Array<RuleNodeComponentDescriptor>;
flowchartConstants = FlowchartConstants;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private route: ActivatedRoute, private route: ActivatedRoute,
private userService: UserService, private router: Router,
private ruleChainService: RuleChainService,
private authService: AuthService, private authService: AuthService,
private translate: TranslateService, private translate: TranslateService,
public dialog: MatDialog, public dialog: MatDialog,
public dialogService: DialogService, public dialogService: DialogService,
public fb: FormBuilder) { public fb: FormBuilder) {
super(store); super(store);
this.init();
} }
ngOnInit() { ngOnInit() {
}
private init() {
this.ruleChain = this.route.snapshot.data.ruleChain; 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() { private loadRuleChainLibrary(ruleNodeComponents: Array<RuleNodeComponentDescriptor>) {
console.log('Model changed!'); 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() { private createRuleChainModel() {
this.model.nodes.push(... this.nextNodeID = 1;
[ this.nextConnectorID = 1;
{
name: 'Node 1', this.selectedObjects.length = 0;
readonly: true, this.ruleChainModel.nodes.length = 0;
id: '2', this.ruleChainModel.edges.length = 0;
x: 300,
y: 100, this.inputConnectorId = this.nextConnectorID++;
color: '#000', this.ruleChainModel.nodes.push(
borderColor: '#000', {
connectors: [ id: 'rule-chain-node-' + this.nextNodeID++,
{ component: inputNodeComponent,
type: FlowchartConstants.leftConnectorType, name: '',
id: '1' nodeClass: ruleNodeTypeDescriptors.get(RuleNodeType.INPUT).nodeClass,
}, icon: ruleNodeTypeDescriptors.get(RuleNodeType.INPUT).icon,
{ readonly: true,
type: FlowchartConstants.rightConnectorType, x: 50,
id: '2' y: 150,
} connectors: [
] {
}, type: FlowchartConstants.rightConnectorType,
{ id: this.inputConnectorId + ''
name: 'Node 2', },
id: '3', ]
x: 600,
y: 100, }
color: '#F15B26', );
connectors: [ const nodes: FcRuleNode[] = [];
{ this.ruleChainMetaData.nodes.forEach((ruleNode) => {
type: FlowchartConstants.leftConnectorType, const component = this.ruleChainService.getRuleNodeComponentByClazz(ruleNode.type);
id: '3' const descriptor = ruleNodeTypeDescriptors.get(component.type);
}, let icon = descriptor.icon;
{ let iconUrl = null;
type: FlowchartConstants.rightConnectorType, if (component.configurationDescriptor.nodeDefinition.icon) {
id: '4' icon = component.configurationDescriptor.nodeDefinition.icon;
} }
] if (component.configurationDescriptor.nodeDefinition.iconUrl) {
}, iconUrl = component.configurationDescriptor.nodeDefinition.iconUrl;
{ }
name: 'Node 3', const node: FcRuleNode = {
id: '4', id: 'rule-chain-node-' + this.nextNodeID++,
x: 1000, ruleNodeId: ruleNode.id,
y: 100, additionalInfo: ruleNode.additionalInfo,
color: '#000', configuration: ruleNode.configuration,
borderColor: '#000', debugMode: ruleNode.debugMode,
connectors: [ x: Math.round(ruleNode.additionalInfo.layoutX),
{ y: Math.round(ruleNode.additionalInfo.layoutY),
type: FlowchartConstants.leftConnectorType, component,
id: '5' name: ruleNode.name,
}, nodeClass: descriptor.nodeClass,
{ icon,
type: FlowchartConstants.rightConnectorType, iconUrl,
id: '6' connectors: []
} };
] if (component.configurationDescriptor.nodeDefinition.inEnabled) {
}, node.connectors.push(
{ {
name: 'Node 4', type: FlowchartConstants.leftConnectorType,
id: '5', id: (this.nextConnectorID++) + ''
x: 1300, }
y: 100, );
color: '#000', }
borderColor: '#000', if (component.configurationDescriptor.nodeDefinition.outEnabled) {
connectors: [ node.connectors.push(
{ {
type: FlowchartConstants.leftConnectorType, type: FlowchartConstants.rightConnectorType,
id: '7' id: (this.nextConnectorID++) + ''
}, }
{ );
type: FlowchartConstants.rightConnectorType, }
id: '8' 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(... if (this.ruleChainMetaData.ruleChainConnections) {
[ const ruleChainsMap = this.ruleChainMetaData.targetRuleChainsMap;
{ const ruleChainNodesMap: {[ruleChainNodeId: string]: FcRuleNode} = {};
source: '2', const ruleChainEdgeMap: {[edgeKey: string]: FcRuleEdge} = {};
destination: '3', this.ruleChainMetaData.ruleChainConnections.forEach((ruleChainConnection) => {
label: 'label1' const ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id];
}, if (ruleChainConnection.additionalInfo && ruleChainConnection.additionalInfo.ruleChainNodeId) {
{ let ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId];
source: '4', if (!ruleChainNode) {
destination: '5', ruleChainNode = {
label: 'label2' id: 'rule-chain-node-' + this.nextNodeID++,
}, name: ruleChain.name ? name : 'Unresolved',
{ targetRuleChainId: ruleChain.name ? ruleChainConnection.targetRuleChainId.id : null,
source: '6', error: ruleChain.name ? undefined : this.translate.instant('rulenode.invalid-target-rulechain'),
destination: '7', additionalInfo: ruleChainConnection.additionalInfo,
label: 'label3' 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;
} }
} }

49
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<FcRuleNodeType>;
}
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<FcRuleNode>;
edges: Array<FcRuleEdge>;
}

80
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 { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; 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 { RuleChainService } from '@core/http/rule-chain.service';
import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component'; import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component';
import { dashboardBreadcumbLabelFunction, DashboardResolver } from '@home/pages/dashboard/dashboard-routing.module'; import { dashboardBreadcumbLabelFunction, DashboardResolver } from '@home/pages/dashboard/dashboard-routing.module';
import { RuleChainPageComponent } from '@home/pages/rulechain/rulechain-page.component'; 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() @Injectable()
@ -45,6 +52,29 @@ export class RuleChainResolver implements Resolve<RuleChain> {
} }
} }
@Injectable()
export class ResolvedRuleChainMetaDataResolver implements Resolve<ResolvedRuleChainMetaData> {
constructor(private ruleChainService: RuleChainService) {
}
resolve(route: ActivatedRouteSnapshot): Observable<ResolvedRuleChainMetaData> {
const ruleChainId = route.params.ruleChainId;
return this.ruleChainService.getResolvedRuleChainMetadata(ruleChainId);
}
}
@Injectable()
export class RuleNodeComponentsResolver implements Resolve<Array<RuleNodeComponentDescriptor>> {
constructor(private ruleChainService: RuleChainService) {
}
resolve(route: ActivatedRouteSnapshot): Observable<Array<RuleNodeComponentDescriptor>> {
return this.ruleChainService.getRuleNodeComponents();
}
}
export const ruleChainBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate, component) => { export const ruleChainBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate, component) => {
let label: string = component.ruleChain.name; let label: string = component.ruleChain.name;
if (component.ruleChain.root) { if (component.ruleChain.root) {
@ -53,6 +83,10 @@ export const ruleChainBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route,
return label; return label;
}); });
export const importRuleChainBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate, component) => {
return `${translate.instant('rulechain.import')}: ${component.ruleChain.name}`;
});
const routes: Routes = [ const routes: Routes = [
{ {
path: 'ruleChains', path: 'ruleChains',
@ -77,6 +111,7 @@ const routes: Routes = [
{ {
path: ':ruleChainId', path: ':ruleChainId',
component: RuleChainPageComponent, component: RuleChainPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: { data: {
breadcrumb: { breadcrumb: {
labelFunction: ruleChainBreadcumbLabelFunction, labelFunction: ruleChainBreadcumbLabelFunction,
@ -84,10 +119,31 @@ const routes: Routes = [
} as BreadCrumbConfig, } as BreadCrumbConfig,
auth: [Authority.TENANT_ADMIN], auth: [Authority.TENANT_ADMIN],
title: 'rulechain.rulechain', title: 'rulechain.rulechain',
widgetEditMode: false import: false
}, },
resolve: { 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], exports: [RouterModule],
providers: [ providers: [
RuleChainsTableConfigResolver, 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 { } export class RuleChainRoutingModule { }

39
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;
}

89
ui-ngx/src/app/shared/models/rule-chain.models.ts

@ -14,10 +14,12 @@
/// limitations under the License. /// limitations under the License.
/// ///
import {BaseData} from '@shared/models/base-data'; import { BaseData } from '@shared/models/base-data';
import {TenantId} from '@shared/models/id/tenant-id'; import { TenantId } from '@shared/models/id/tenant-id';
import {RuleChainId} from '@shared/models/id/rule-chain-id'; import { RuleChainId } from '@shared/models/id/rule-chain-id';
import {RuleNodeId} from '@shared/models/id/rule-node-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<RuleChainId> { export interface RuleChain extends BaseData<RuleChainId> {
tenantId: TenantId; tenantId: TenantId;
@ -28,3 +30,82 @@ export interface RuleChain extends BaseData<RuleChainId> {
configuration?: any; configuration?: any;
additionalInfo?: any; additionalInfo?: any;
} }
export interface RuleChainMetaData {
ruleChainId: RuleChainId;
firstNodeIndex: number;
nodes: Array<RuleNode>;
connections: Array<NodeConnectionInfo>;
ruleChainConnections: Array<RuleChainConnectionInfo>;
}
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'
};

131
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 {CustomerId} from '@shared/models/id/customer-id';
import {RuleChainId} from '@shared/models/id/rule-chain-id'; import {RuleChainId} from '@shared/models/id/rule-chain-id';
import {RuleNodeId} from '@shared/models/id/rule-node-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 { export enum MsgDataType {
JSON = 'JSON', JSON = 'JSON',
@ -28,7 +30,7 @@ export enum MsgDataType {
} }
export interface RuleNodeConfiguration { export interface RuleNodeConfiguration {
todo: Array<any>; [key: string]: any;
// TODO: // TODO:
} }
@ -40,3 +42,130 @@ export interface RuleNode extends BaseData<RuleNodeId> {
configuration: RuleNodeConfiguration; configuration: RuleNodeConfiguration;
additionalInfo?: any; 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, RuleNodeTypeDescriptor>(
[
[
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;
}

Loading…
Cancel
Save