Browse Source

UI: Create nested rule chain from selected region

pull/5707/head
Igor Kulikov 5 years ago
parent
commit
a5b747107b
  1. 65
      ui-ngx/src/app/modules/home/pages/rulechain/create-nested-rulechain-dialog.component.html
  2. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss
  3. 298
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  4. 5
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts
  5. 3
      ui-ngx/src/app/shared/models/constants.ts
  6. 8
      ui-ngx/src/app/shared/models/rule-node.models.ts
  7. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json

65
ui-ngx/src/app/modules/home/pages/rulechain/create-nested-rulechain-dialog.component.html

@ -0,0 +1,65 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form (ngSubmit)="add()" style="min-width: 650px;">
<mat-toolbar color="primary">
<h2 translate>rulenode.create-nested-rulechain</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<form [formGroup]="createNestedRuleChainFormGroup" class="mat-padding">
<fieldset [disabled]="isLoading$ | async">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>rulechain.name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="createNestedRuleChainFormGroup.get('name').hasError('required')">
{{ 'rulechain.name-required' | translate }}
</mat-error>
<mat-error *ngIf="createNestedRuleChainFormGroup.get('name').hasError('maxlength')">
{{ 'rulechain.name-max-length' | translate }}
</mat-error>
</mat-form-field>
<div formGroupName="additionalInfo">
<mat-form-field class="mat-block">
<mat-label translate>rulechain.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</div>
</fieldset>
</form>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || createNestedRuleChainFormGroup.invalid || !createNestedRuleChainFormGroup.dirty">
{{ 'action.create' | translate }}
</button>
</div>
</form>

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

@ -325,7 +325,7 @@
}
.tb-rule-chain-context-menu {
min-width: 256px;
min-width: 320px;
max-height: 404px;
border-radius: 8px;
margin-left: -20px;

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

@ -54,7 +54,7 @@ import {
FcRuleNode,
FcRuleNodeType,
getRuleNodeHelpLink,
LinkLabel,
LinkLabel, outputNodeClazz, ruleChainNodeClazz,
RuleNode,
RuleNodeComponentDescriptor,
RuleNodeType,
@ -405,6 +405,17 @@ export class RuleChainPageComponent extends PageComponent
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('rulenode.delete-selected-objects'))
);
this.hotKeys.push(
new Hotkey('ctrl+r', (event: KeyboardEvent) => {
if (this.enableHotKeys && this.canCreateNestedRuleChain()) {
event.preventDefault();
this.createNestedRuleChain();
return false;
}
return true;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('rulenode.create-nested-rulechain'))
);
}
}
@ -684,6 +695,19 @@ export class RuleChainPageComponent extends PageComponent
shortcut: 'Esc'
}
);
if (this.canCreateNestedRuleChain()) {
contextInfo.menuItems.push(
{
action: () => {
this.createNestedRuleChain();
},
enabled: true,
value: 'rulenode.create-nested-rulechain',
icon: 'settings_ethernet',
shortcut: 'M-R'
}
);
}
contextInfo.menuItems.push(
{
action: () => {
@ -818,6 +842,208 @@ export class RuleChainPageComponent extends PageComponent
return contextInfo;
}
private canCreateNestedRuleChain(): boolean {
const selectedNodes = this.ruleChainCanvas.modelService.nodes.getSelectedNodes();
const selectedEdges = this.ruleChainCanvas.modelService.edges.getSelectedEdges();
if (selectedNodes.length > 1) {
const toIndexSet = new Set<number>();
selectedEdges.forEach((edge: FcRuleEdge) => {
const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source);
const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination);
const fromIndex = selectedNodes.indexOf(sourceNode);
const toIndex = selectedNodes.indexOf(destNode);
if (fromIndex > -1 && toIndex > -1) {
toIndexSet.add(toIndex);
}
});
const noInputNodes = selectedNodes.filter((node, index) => !toIndexSet.has(index));
return noInputNodes.filter((node: FcRuleNode) => node.component.configurationDescriptor.nodeDefinition.inEnabled).length <= 1;
}
return false;
}
private createNestedRuleChain() {
const selectedNodes = this.ruleChainCanvas.modelService.nodes.getSelectedNodes();
const selectedEdges = this.ruleChainCanvas.modelService.edges.getSelectedEdges();
this.dialog.open<CreateNestedRuleChainDialogComponent, CreateNestedRuleChainDialogData,
RuleChain>(CreateNestedRuleChainDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
ruleChainType: this.ruleChainType
}
}).afterClosed().subscribe((ruleChain) => {
if (ruleChain) {
this.ruleChainCanvas.modelService.deselectAll();
const ruleChainMetaData: RuleChainMetaData = {
ruleChainId: ruleChain.id,
nodes: [],
connections: []
};
let outputEdges: FcRuleEdge[] = [];
let minX: number = null;
let maxX = 0;
let minY = null;
let maxY = 0;
selectedNodes.forEach((node: FcRuleNode) => {
const ruleNode: RuleNode = {
type: node.component.clazz,
name: node.name,
configuration: deepClone(node.configuration),
additionalInfo: node.additionalInfo ? deepClone(node.additionalInfo) : {},
debugMode: node.debugMode
};
if (minX === null) {
minX = node.x;
} else {
minX = Math.min(minX, node.x);
}
if (minY === null) {
minY = node.y;
} else {
minY = Math.min(minY, node.y);
}
maxX = Math.max(maxX, node.x);
maxY = Math.max(maxY, node.y);
ruleNode.additionalInfo.layoutX = Math.round(node.x);
ruleNode.additionalInfo.layoutY = Math.round(node.y);
ruleChainMetaData.nodes.push(ruleNode);
const outputConnectors = this.ruleChainCanvas.modelService.nodes.getConnectorsByType(node, FlowchartConstants.rightConnectorType);
outputConnectors.forEach(connector => {
const nodeOutputEdges = this.ruleChainCanvas.modelService.model.edges.filter(edge => edge.source === connector.id);
const outerEdges = nodeOutputEdges.filter(edge => {
const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination);
return selectedNodes.indexOf(destNode) === -1;
});
outputEdges = outputEdges.concat(outerEdges);
});
});
const toIndexSet = new Set<number>();
selectedEdges.forEach((edge: FcRuleEdge) => {
const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source);
const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination);
if (sourceNode.component.type !== RuleNodeType.INPUT) {
const fromIndex = selectedNodes.indexOf(sourceNode);
const toIndex = selectedNodes.indexOf(destNode);
if (fromIndex > -1 && toIndex > -1) {
const nodeConnection = {
fromIndex,
toIndex
} as NodeConnectionInfo;
edge.labels.forEach((label) => {
const newNodeConnection = deepClone(nodeConnection);
newNodeConnection.type = label;
ruleChainMetaData.connections.push(newNodeConnection);
});
toIndexSet.add(toIndex);
}
}
});
const noInputNodes = selectedNodes.filter((node, index) => !toIndexSet.has(index));
const possibleInputNodes = noInputNodes.filter((node: FcRuleNode) =>
node.component.configurationDescriptor.nodeDefinition.inEnabled);
let inputEdges: FcRuleEdge[] = [];
if (possibleInputNodes.length) {
const firstNode = possibleInputNodes[0];
const inputConnectors = this.ruleChainCanvas.modelService.nodes
.getConnectorsByType(firstNode, FlowchartConstants.leftConnectorType);
if (inputConnectors.length) {
const inputConnector = inputConnectors[0];
const nodeInputEdges = this.ruleChainCanvas.modelService.model.edges.filter(edge => edge.destination === inputConnector.id);
inputEdges = inputEdges.concat(nodeInputEdges);
}
ruleChainMetaData.firstNodeIndex = selectedNodes.indexOf(firstNode);
}
outputEdges.forEach((outputEdge) => {
const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(outputEdge.source);
const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(outputEdge.destination);
const outputNode: RuleNode = {
type: outputNodeClazz,
name: outputEdge.label,
configuration: {},
additionalInfo: {},
debugMode: false
};
outputNode.additionalInfo.layoutX = Math.round(destNode.x);
outputNode.additionalInfo.layoutY = Math.round(destNode.y);
ruleChainMetaData.nodes.push(outputNode);
const fromIndex = selectedNodes.indexOf(sourceNode);
const toIndex = ruleChainMetaData.nodes.length - 1;
const nodeConnection = {
fromIndex,
toIndex
} as NodeConnectionInfo;
outputEdge.labels.forEach((label) => {
const newNodeConnection = deepClone(nodeConnection);
newNodeConnection.type = label;
ruleChainMetaData.connections.push(newNodeConnection);
});
});
const deltaX = Math.round(minX - 375);
const deltaY = Math.round(minY - 150);
ruleChainMetaData.nodes.forEach((node) => {
node.additionalInfo.layoutX -= deltaX;
node.additionalInfo.layoutY -= deltaY;
});
this.ruleChainService.saveRuleChainMetadata(ruleChainMetaData).subscribe(() => {
const component = this.ruleChainService.getRuleNodeComponentByClazz(this.ruleChainType, ruleChainNodeClazz);
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 ruleChainNodeX = (minX + maxX) / 2;
const ruleChainNodeY = (minY + maxY) / 2;
const ruleChainInputId = (this.nextConnectorID++) + '';
const ruleChainOutputId = (this.nextConnectorID++) + '';
const ruleChainNode: FcRuleNode = {
name: ruleChain.name,
component,
id: 'rule-chain-node-' + this.nextNodeID++,
configuration: {
ruleChainId: ruleChain.id.id
},
debugMode: false,
x: Math.round(ruleChainNodeX),
y: Math.round(ruleChainNodeY),
nodeClass: descriptor.nodeClass,
icon,
iconUrl,
ruleChainType: this.ruleChainType,
connectors: [
{
type: FlowchartConstants.leftConnectorType,
id: ruleChainInputId
},
{
type: FlowchartConstants.rightConnectorType,
id: ruleChainOutputId
}
]
};
this.ruleChainModel.nodes.push(ruleChainNode);
inputEdges.forEach((inputEdge) => {
inputEdge.destination = ruleChainInputId;
});
outputEdges.forEach((outputEdge) => {
outputEdge.source = ruleChainOutputId;
outputEdge.labels = [outputEdge.label];
});
selectedNodes.forEach((node) => {
this.ruleChainCanvas.modelService.nodes.delete(node);
});
this.onModelChanged();
this.updateRuleNodesHighlight();
});
}
});
}
onModelChanged() {
this.isDirtyValue = true;
this.validate();
@ -1498,3 +1724,73 @@ export class AddRuleNodeDialogComponent extends DialogComponent<AddRuleNodeDialo
}
}
}
export interface CreateNestedRuleChainDialogData {
ruleChainType: RuleChainType;
}
@Component({
selector: 'tb-create-nested-rulechain-dialog',
templateUrl: './create-nested-rulechain-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: CreateNestedRuleChainDialogComponent}],
styleUrls: []
})
export class CreateNestedRuleChainDialogComponent extends DialogComponent<CreateNestedRuleChainDialogComponent, RuleChain>
implements OnInit, ErrorStateMatcher {
createNestedRuleChainFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CreateNestedRuleChainDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
private fb: FormBuilder,
private ruleChainService: RuleChainService,
public dialogRef: MatDialogRef<CreateNestedRuleChainDialogComponent, RuleChain>) {
super(store, router, dialogRef);
}
ngOnInit(): void {
this.createNestedRuleChainFormGroup = this.fb.group(
{
name: ['', [Validators.required, Validators.maxLength(255)]],
additionalInfo: this.fb.group(
{
description: [''],
}
)
}
);
}
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 ruleChain = {
name: this.createNestedRuleChainFormGroup.get('name').value,
debugMode: false,
type: this.data.ruleChainType,
additionalInfo: {
description: this.createNestedRuleChainFormGroup.get('additionalInfo').get('description').value
}
} as RuleChain;
this.ruleChainService.saveRuleChain(ruleChain).subscribe(
(savedRuleChain) => {
this.dialogRef.close(savedRuleChain);
}
);
}
}

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

@ -23,7 +23,7 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m
import { RuleChainTabsComponent } from '@home/pages/rulechain/rulechain-tabs.component';
import {
AddRuleNodeDialogComponent,
AddRuleNodeLinkDialogComponent,
AddRuleNodeLinkDialogComponent, CreateNestedRuleChainDialogComponent,
RuleChainPageComponent
} from './rulechain-page.component';
import { RuleNodeComponent } from '@home/pages/rulechain/rulenode.component';
@ -44,7 +44,8 @@ import { RuleNodeConfigComponent } from './rule-node-config.component';
LinkLabelsComponent,
RuleNodeLinkComponent,
AddRuleNodeLinkDialogComponent,
AddRuleNodeDialogComponent
AddRuleNodeDialogComponent,
CreateNestedRuleChainDialogComponent
],
providers: [
{

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

@ -100,7 +100,8 @@ export const HelpLinks = {
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',
ruleNodeSaveToCustomTable: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#save-to-custom-table',
ruleNodeRuleChain: helpBaseUrl + '/docs/user-guide/ui/rule-chains/',
ruleNodeRuleChain: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/flow-nodes/#rule-chain-node',
ruleNodeOutputNode: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/flow-nodes/#output-node',
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',

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

@ -394,6 +394,9 @@ export const messageTypeNames = new Map<MessageType, string>(
]
);
export const ruleChainNodeClazz = 'org.thingsboard.rule.engine.flow.TbRuleChainInputNode';
export const outputNodeClazz = 'org.thingsboard.rule.engine.flow.TbRuleChainOutputNode';
const ruleNodeClazzHelpLinkMap = {
'org.thingsboard.rule.engine.filter.TbCheckRelationNode': 'ruleNodeCheckRelation',
'org.thingsboard.rule.engine.filter.TbCheckMessageNode': 'ruleNodeCheckExistenceFields',
@ -431,7 +434,6 @@ const ruleNodeClazzHelpLinkMap = {
'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode': 'ruleNodeSaveAttributes',
'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode': 'ruleNodeSaveTimeseries',
'org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode': 'ruleNodeSaveToCustomTable',
'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',
@ -442,7 +444,9 @@ const ruleNodeClazzHelpLinkMap = {
'org.thingsboard.rule.engine.mail.TbSendEmailNode': 'ruleNodeSendEmail',
'org.thingsboard.rule.engine.sms.TbSendSmsNode': 'ruleNodeSendSms',
'org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode': 'ruleNodePushToCloud',
'org.thingsboard.rule.engine.edge.TbMsgPushToEdgeNode': 'ruleNodePushToEdge'
'org.thingsboard.rule.engine.edge.TbMsgPushToEdgeNode': 'ruleNodePushToEdge',
'org.thingsboard.rule.engine.flow.TbRuleChainInputNode': 'ruleNodeRuleChain',
'org.thingsboard.rule.engine.flow.TbRuleChainOutputNode': 'ruleNodeOutputNode',
};
export function getRuleNodeHelpLink(component: RuleNodeComponentDescriptor): string {

1
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2587,6 +2587,7 @@
"deselect-all-objects": "Deselect all nodes and connections",
"delete-selected-objects": "Delete selected nodes and connections",
"delete-selected": "Delete selected",
"create-nested-rulechain": "Create nested rule chain",
"select-all": "Select all",
"copy-selected": "Copy selected",
"deselect-all": "Deselect all",

Loading…
Cancel
Save