summaryrefslogtreecommitdiffstats
path: root/public/src/app/rule-engine
diff options
context:
space:
mode:
Diffstat (limited to 'public/src/app/rule-engine')
-rw-r--r--public/src/app/rule-engine/action-list/action-list.component.html100
-rw-r--r--public/src/app/rule-engine/action-list/action-list.component.scss77
-rw-r--r--public/src/app/rule-engine/action-list/action-list.component.ts290
-rw-r--r--public/src/app/rule-engine/action/action.component.html114
-rw-r--r--public/src/app/rule-engine/action/action.component.scss116
-rw-r--r--public/src/app/rule-engine/action/action.component.ts51
-rw-r--r--public/src/app/rule-engine/api/rule-engine-api.service.spec.ts19
-rw-r--r--public/src/app/rule-engine/api/rule-engine-api.service.ts134
-rw-r--r--public/src/app/rule-engine/condition/condition.component.html89
-rw-r--r--public/src/app/rule-engine/condition/condition.component.scss114
-rw-r--r--public/src/app/rule-engine/condition/condition.component.spec.ts51
-rw-r--r--public/src/app/rule-engine/condition/condition.component.ts161
-rw-r--r--public/src/app/rule-engine/confirm-popup/confirm-popup.component.html12
-rw-r--r--public/src/app/rule-engine/confirm-popup/confirm-popup.component.scss20
-rw-r--r--public/src/app/rule-engine/confirm-popup/confirm-popup.component.ts18
-rw-r--r--public/src/app/rule-engine/from/from.component.html70
-rw-r--r--public/src/app/rule-engine/from/from.component.scss63
-rw-r--r--public/src/app/rule-engine/from/from.component.ts91
-rw-r--r--public/src/app/rule-engine/host/exit-mode.enum.ts4
-rw-r--r--public/src/app/rule-engine/host/host-params.ts8
-rw-r--r--public/src/app/rule-engine/host/host.service.spec.ts18
-rw-r--r--public/src/app/rule-engine/host/host.service.ts56
-rw-r--r--public/src/app/rule-engine/rule-list/rule-list.component.html73
-rw-r--r--public/src/app/rule-engine/rule-list/rule-list.component.scss109
-rw-r--r--public/src/app/rule-engine/rule-list/rule-list.component.ts197
-rw-r--r--public/src/app/rule-engine/slide-panel/slide-panel.component.html8
-rw-r--r--public/src/app/rule-engine/slide-panel/slide-panel.component.scss15
-rw-r--r--public/src/app/rule-engine/slide-panel/slide-panel.component.ts27
-rw-r--r--public/src/app/rule-engine/target/target.component.html28
-rw-r--r--public/src/app/rule-engine/target/target.component.scss99
-rw-r--r--public/src/app/rule-engine/target/target.component.spec.ts57
-rw-r--r--public/src/app/rule-engine/target/target.component.ts77
-rw-r--r--public/src/app/rule-engine/target/target.util.ts50
-rw-r--r--public/src/app/rule-engine/target/target.validation.spec.ts83
-rw-r--r--public/src/app/rule-engine/version-type-select/version-type-select.component.html34
-rw-r--r--public/src/app/rule-engine/version-type-select/version-type-select.component.scss46
-rw-r--r--public/src/app/rule-engine/version-type-select/version-type-select.component.ts86
37 files changed, 2665 insertions, 0 deletions
diff --git a/public/src/app/rule-engine/action-list/action-list.component.html b/public/src/app/rule-engine/action-list/action-list.component.html
new file mode 100644
index 0000000..e7879b7
--- /dev/null
+++ b/public/src/app/rule-engine/action-list/action-list.component.html
@@ -0,0 +1,100 @@
+<form #actionListFrm="ngForm" class="wrapper" data-tests-id="popupRuleEditor">
+ <div class="header">
+ <div style="display: flex; justify-content: flex-end; align-items: center;">
+ <a (click)="closeDialog()" data-tests-id="btnBackRule" style="cursor: pointer;text-decoration: none; color: #009fdb;">
+ <mat-icon fontSet="fontawesome" fontIcon="fa-angle-left" style="height: 22px; width: 22px; font-size: 22px; padding-right: 20px;"></mat-icon>
+ </a>
+ <span style="font-size: 18px;">New Rule Editor</span>
+ </div>
+
+ <div style="display: flex; justify-content: flex-end; align-items: center; padding: 10px;">
+
+ <button mat-icon-button [disabled]="actions.length === 0" (click)="saveRole()" data-tests-id="btnSave">
+ <span style="width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;" [innerHTML]="'save' | feather:22"></span>
+ </button>
+
+ <button mat-raised-button [disabled]="actions.length === 0" style="height: 35px; margin-left: 20px;" color="primary" data-tests-id="btnDone"
+ (click)="saveAndDone()">
+ Done
+ </button>
+ </div>
+ </div>
+ <!-- error container -->
+ <div *ngIf="error" data-tests-id="errorList" class="error-list">
+ <div *ngFor="let item of error">
+ {{ item }}
+ </div>
+ </div>
+
+ <div class="main-content">
+ <div>
+ <div class="required" style="padding-right: 1rem; width: 100%; padding-bottom: 0.5rem;">Description</div>
+ <input type="text" [(ngModel)]="description" ngModel required name="descInput" style="padding: 5px; width: 100%;" data-tests-id="inputDescription">
+ </div>
+
+ <div style="margin: 1.5rem 0;">
+ <div class="pretty p-svg" style="margin: 1rem 0rem;">
+ <input type="checkbox" name="isCondition" data-tests-id="isCondition" [checked]="ifStatement" (change)="ifStatement = !ifStatement"
+ />
+ <div class="state">
+ <!-- svg path -->
+ <svg class="svg svg-icon" viewBox="0 0 20 20">
+ <path d="M7.629,14.566c0.125,0.125,0.291,0.188,0.456,0.188c0.164,0,0.329-0.062,0.456-0.188l8.219-8.221c0.252-0.252,0.252-0.659,0-0.911c-0.252-0.252-0.659-0.252-0.911,0l-7.764,7.763L4.152,9.267c-0.252-0.251-0.66-0.251-0.911,0c-0.252,0.252-0.252,0.66,0,0.911L7.629,14.566z"
+ style="stroke: #009fdb; fill:#009fdb;"></path>
+ </svg>
+ <label>Conditional Action</label>
+ </div>
+ </div>
+
+ <div *ngIf="ifStatement">
+ <app-condition #condition (removeConditionCheck)="removeConditionCheck($event)" (onConditionChange)="updateCondition($event)"></app-condition>
+ </div>
+ </div>
+
+ <div>
+ <div class="required" style="padding-bottom: 0.5rem">
+ Action
+ </div>
+ <div style="display: flex;">
+ <select [(ngModel)]="selectedAction" name="selectedAction" style="height: 2rem; width: 150px; margin-right: 1rem;" data-tests-id="selectAction">
+ <option [ngValue]="null" disabled>Select Action</option>
+ <option value="copy">Copy</option>
+ <option value="concat">Concat</option>
+ <option value="map">Map</option>
+ <option value="date formatter">Date Formatter</option>
+ </select>
+
+ <div style="display: flex; align-items: center;">
+ <button mat-mini-fab color="primary" style="height: 24px; width: 24px; display:flex; justify-content: center;" (click)="addAction2list(selectedAction)"
+ data-tests-id="btnAddAction">
+ <span style="display: flex; justify-content: center; align-items: center" [innerHTML]="'plus' | feather:16"></span>
+ </button>
+ <span style="color: #009FDB; display: flex; justify-content: center; padding-left: 10px">Add Action</span>
+ </div>
+
+ </div>
+
+ <div>
+ <ul>
+ <li *ngFor="let action of actions; let index = index" style="list-style: none; margin: 1rem 0;" (mouseleave)="hoveredIndex=-1"
+ (mouseover)="hoveredIndex=index" data-tests-id="action">
+ <div style="display:flex;">
+ <app-action #actions style="width: 100%;" [action]="action"></app-action>
+
+ <div style="height: 45px; display: flex; align-items: center;">
+ <button mat-icon-button class='button-remove' (click)="removeAction(action)" data-tests-id="deleteAction">
+ <mat-icon>delete</mat-icon>
+ </button>
+ </div>
+ </div>
+ </li>
+ </ul>
+ </div>
+
+ </div>
+ </div>
+</form>
diff --git a/public/src/app/rule-engine/action-list/action-list.component.scss b/public/src/app/rule-engine/action-list/action-list.component.scss
new file mode 100644
index 0000000..39b9dce
--- /dev/null
+++ b/public/src/app/rule-engine/action-list/action-list.component.scss
@@ -0,0 +1,77 @@
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 100%;
+
+ .header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ color: #191919;
+ border-bottom: 2px solid #d2d2d2;
+ // padding: 0.4rem 1rem;
+ }
+
+ .main-content {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ flex-grow: 1;
+ // overflow-y: auto;
+ padding: 24px 10px;
+ width: 100%;
+ // height: calc(100vh - 54px);
+ }
+}
+
+.mat-fab,
+.mat-mini-fab,
+.mat-raised-button {
+ box-shadow: none;
+}
+
+.button-remove {
+ display: flex;
+ justify-content: center;
+ padding-top: 5px;
+ color: #a7a7a7;
+ &:hover {
+ color: #009fdb;
+ }
+}
+
+:host {
+ @mixin md-icon-size($size: 24px) {
+ // font-size: $size;
+ height: $size;
+ width: $size;
+ }
+
+ .material-icons.mat-icon {
+ @include md-icon-size(24px);
+ }
+ /deep/ .mat-button-wrapper {
+ padding: 0;
+ }
+ .mat-icon {
+ width: 18px;
+ height: 18px;
+ }
+}
+
+.black {
+ color: black;
+}
+.highlight {
+ color: #009fdb;
+}
+
+.error-list {
+ margin: 10px;
+ color: white;
+ background: red;
+ padding: 1rem;
+ border-radius: 5px;
+ font-weight: bold;
+}
diff --git a/public/src/app/rule-engine/action-list/action-list.component.ts b/public/src/app/rule-engine/action-list/action-list.component.ts
new file mode 100644
index 0000000..40ff46d
--- /dev/null
+++ b/public/src/app/rule-engine/action-list/action-list.component.ts
@@ -0,0 +1,290 @@
+import {
+ Component,
+ Inject,
+ ViewChildren,
+ QueryList,
+ AfterViewInit,
+ ViewChild,
+ Input
+} from '@angular/core';
+import { RuleEngineApiService } from '../api/rule-engine-api.service';
+import { Subject } from 'rxjs/Subject';
+import { v1 as uuid } from 'uuid';
+import { environment } from '../../../environments/environment';
+import { ActionComponent } from '../action/action.component';
+import { cloneDeep } from 'lodash';
+import { Store } from '../../store/store';
+import { NgForm } from '@angular/forms';
+
+@Component({
+ selector: 'app-action-list',
+ templateUrl: './action-list.component.html',
+ styleUrls: ['./action-list.component.scss']
+})
+export class ActionListComponent implements AfterViewInit {
+ error: Array<string>;
+ condition: any;
+ eventType: string;
+ version: string;
+ params;
+ selectedAction;
+ targetSource;
+ description = '';
+ actions = new Array();
+ ifStatement = false;
+ uid = '';
+ backupActionForCancel = new Array();
+ @ViewChild('actionListFrm') actionListFrm: NgForm;
+ @ViewChild('condition') conditionRef;
+ @ViewChildren('actions') actionsRef: QueryList<ActionComponent>;
+
+ constructor(private _ruleApi: RuleEngineApiService, public store: Store) {
+ this._ruleApi.editorData.subscribe(data => {
+ this.params = data.params;
+ console.log('update.. params', data.params);
+ this.targetSource = data.targetSource;
+ this.version = data.version;
+ this.eventType = data.eventType;
+ if (data.item) {
+ // edit mode set values to attributes
+ console.log('actions %o', data.item.actions);
+ this.actions = this.convertActionDataFromServer(data.item.actions);
+ this.backupActionForCancel = cloneDeep(this.actions);
+ this.condition = data.item.condition;
+ this.uid = data.item.uid;
+ this.description = data.item.description;
+ this.ifStatement = this.condition == null ? false : true;
+ } else {
+ this.actions = new Array();
+ this.backupActionForCancel = new Array();
+ this.condition = null;
+ this.uid = '';
+ this.description = '';
+ this.ifStatement = false;
+ }
+ this.selectedAction = null;
+ });
+ }
+
+ convertActionDataFromServer(actions) {
+ return actions.map(item => {
+ if (!item.hasOwnProperty('nodes')) {
+ return Object.assign({}, item, { nodes: this.targetSource });
+ }
+ });
+ }
+
+ ngAfterViewInit() {
+ // console.log(this.actionsRef.toArray());
+ if (this.condition) {
+ if (this.condition.name === 'condition') {
+ this.conditionRef.updateMode(true, this.condition);
+ } else {
+ const convertedCondition = this.convertConditionFromServer(
+ this.condition
+ );
+ this.conditionRef.updateMode(false, convertedCondition);
+ }
+ }
+ }
+
+ addAction2list(selectedAction) {
+ if (selectedAction !== null) {
+ this.actions.push({
+ id: uuid(),
+ nodes: this.targetSource,
+ from: {
+ value: '',
+ regex: '',
+ state: 'closed',
+ values: [{ value: '' }, { value: '' }]
+ },
+ actionType: this.selectedAction,
+ target: '',
+ map: {
+ values: [{ key: '', value: '' }],
+ haveDefault: false,
+ default: ''
+ },
+ dateFormatter: {
+ fromFormat: '',
+ toFormat: '',
+ fromTimezone: '',
+ toTimezone: ''
+ }
+ });
+ }
+ }
+
+ removeConditionCheck(flag) {
+ this.ifStatement = flag;
+ }
+
+ removeAction(action) {
+ this.actions = this.actions.filter(item => {
+ return item.id !== action.id;
+ });
+ }
+
+ updateCondition(data) {
+ this.condition = data;
+ }
+
+ changeRightToArrayOrString(data, toArray) {
+ data.forEach(element => {
+ if (element.name === 'operator') {
+ this.changeRightToArrayOrString(element.children, toArray);
+ }
+ if (element.name === 'condition') {
+ if (toArray) {
+ element.right = element.right.split(',');
+ } else {
+ element.right = element.right.join(',');
+ }
+ }
+ });
+ console.log(data);
+ return data;
+ }
+
+ prepareDataToSaveRule() {
+ // action array
+ console.log(this.actions);
+ const actionSetData = this.actions.map(item => {
+ return {
+ id: item.id,
+ actionType: item.actionType,
+ from: item.from,
+ target:
+ typeof item.selectedNode === 'string'
+ ? item.selectedNode
+ : typeof item.selectedNode === 'undefined'
+ ? item.target
+ : item.selectedNode.id,
+ map: item.map,
+ dateFormatter: item.dateFormatter
+ };
+ });
+ let conditionData2server = null;
+ if (this.ifStatement) {
+ if (this.conditionRef.conditionTree) {
+ // change condition right to array
+ conditionData2server = this.convertConditionToServer(
+ this.conditionRef.conditionTree
+ );
+ }
+ }
+ // data structure
+ return {
+ version: this.version,
+ eventType: this.eventType,
+ uid: this.uid,
+ description: this.description,
+ actions: actionSetData,
+ condition: this.ifStatement ? conditionData2server : null
+ };
+ }
+
+ errorHandler(error) {
+ this.store.loader = false;
+ console.log(error);
+ this.error = [];
+ if (typeof error === 'string') {
+ this.error.push(error);
+ } else {
+ console.log(error.notes);
+ const errorFromServer = Object.values(error)[0] as any;
+ if (Object.keys(error)[0] === 'serviceExceptions') {
+ this.error = errorFromServer.map(x => x.formattedErrorMessage);
+ } else {
+ this.error.push(errorFromServer.formattedErrorMessage);
+ }
+ }
+ }
+
+ saveAndDone() {
+ const data = this.prepareDataToSaveRule();
+ this.store.loader = true;
+ this._ruleApi.modifyRule(data).subscribe(
+ response => {
+ this.store.loader = false;
+ this.store.updateRuleInList(response);
+ this._ruleApi.callUpdateVersionLock();
+ this.store.isLeftVisible = true;
+ },
+ error => {
+ this.errorHandler(error);
+ },
+ () => {
+ this.store.loader = false;
+ }
+ );
+ }
+
+ saveRole() {
+ const actionComp = this.actionsRef.toArray();
+ const filterInvalidActions = actionComp.filter(comp => {
+ return (
+ comp.fromInstance.fromFrm.invalid ||
+ comp.targetInstance.targetFrm.invalid ||
+ comp.actionFrm.invalid
+ );
+ });
+ if (this.actionListFrm.valid && filterInvalidActions.length === 0) {
+ const data = this.prepareDataToSaveRule();
+ this.store.loader = true;
+ this._ruleApi.modifyRule(data).subscribe(
+ response => {
+ this.store.loader = false;
+ this.store.updateRuleInList(response);
+ this._ruleApi.callUpdateVersionLock();
+ this.uid = response.uid;
+ // add toast notification
+ },
+ error => {
+ this.errorHandler(error);
+ },
+ () => {
+ this.store.loader = false;
+ }
+ );
+ } else {
+ // scroll to first invalid element
+ const elId = filterInvalidActions[0].action.id;
+ const el = document.getElementById(elId as string);
+ const label = el.children.item(0) as HTMLElement;
+ el.scrollIntoView();
+ }
+ }
+
+ public convertConditionFromServer(condition) {
+ const temp = new Array();
+ temp.push(condition);
+ const cloneCondition = cloneDeep(temp);
+ const conditionSetData = this.changeRightToArrayOrString(
+ cloneCondition,
+ false
+ );
+ console.log('condition to server:', conditionSetData);
+ return conditionSetData;
+ }
+
+ public convertConditionToServer(tree) {
+ const cloneCondition = cloneDeep(tree);
+ const conditionSetData = this.changeRightToArrayOrString(
+ cloneCondition,
+ true
+ );
+ let simpleCondition = null;
+ if (conditionSetData[0].children.length === 1) {
+ simpleCondition = conditionSetData[0].children;
+ }
+ console.log('condition to server:', conditionSetData);
+ return simpleCondition !== null ? simpleCondition[0] : conditionSetData[0];
+ }
+
+ closeDialog(): void {
+ this.actions = this.backupActionForCancel;
+ this.store.isLeftVisible = true;
+ }
+}
diff --git a/public/src/app/rule-engine/action/action.component.html b/public/src/app/rule-engine/action/action.component.html
new file mode 100644
index 0000000..b41ab82
--- /dev/null
+++ b/public/src/app/rule-engine/action/action.component.html
@@ -0,0 +1,114 @@
+<form #actionFrm="ngForm" class="conatiner" id="{{action.id}}" (mouseover)="changeStyle($event)" (mouseout)="changeStyle($event)">
+ <div>
+ <div class="center-content">
+ <!-- type info -->
+ <div class="action-info" [ngClass]="highlight">
+ {{action.actionType | uppercase}}
+ </div>
+ <!-- from component -->
+ <app-from #from style="width: 100%" [actionType]="action.actionType" (onFromChange)="updateFrom($event)"></app-from>
+ <!-- target component -->
+ <app-target #target style="width: 100%" (onTargetChange)="updateTarget($event)" [nodes]="action.nodes">
+ </app-target>
+ </div>
+
+ <!-- dateFormatter -->
+ <div *ngIf="action.actionType === 'date formatter'" style="display: flex; flex-direction: column; margin: 1em; align-items: flex-end;">
+ <div style="display: flex; margin: 0.5em 0;">
+ <div class="from">
+ <div class="from-conatiner">
+ <div style="display: flex; align-items: center;" class="label">
+ <span class="label" style="padding: 0 5px; width: 100px;">From Format</span>
+ <input class="input-text" ngModel required name="fromFormat" [(ngModel)]="action.dateFormatter.fromFormat" type="text">
+ </div>
+ </div>
+ </div>
+ <div class="from">
+ <div class="from-conatiner">
+ <div style="display: flex; align-items: center;" class="label">
+ <span class="label" style="padding: 0 5px; width: 100px;">To Format</span>
+ <input class="input-text" ngModel required name="toFormat" [(ngModel)]="action.dateFormatter.toFormat" type="text">
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div style="display: flex; margin: 0.5em 0;">
+ <div class="from">
+ <div class="from-conatiner">
+ <div style="display: flex; align-items: center;" class="label">
+ <span class="label" style="padding: 0 5px; width: 100px;">From Time-zone</span>
+ <input class="input-text" ngModel required name="fromTimezone" [(ngModel)]="action.dateFormatter.fromTimezone" type="text">
+ </div>
+ </div>
+ </div>
+ <div class="from">
+ <div class="from-conatiner">
+ <div style="display: flex; align-items: center;" class="label">
+ <span class="label" style="padding: 0 5px; width: 100px;">To Time-zone</span>
+ <input class="input-text" ngModel required name="toTimezone" [(ngModel)]="action.dateFormatter.toTimezone" type="text">
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Map -->
+ <div *ngIf="action.actionType === 'map'" class="map-container">
+ <!-- Default checkbox and input -->
+ <div class="default" style="display: flex; align-items: center">
+ <div class="pretty p-svg">
+ <input type="checkbox" name="defaultCheckbox" data-tests-id="defaultCheckbox" [checked]="action.map.haveDefault" (change)="changeCheckbox()"
+ />
+ <div class="state">
+ <!-- svg path -->
+ <svg class="svg svg-icon" viewBox="0 0 20 20">
+ <path d="M7.629,14.566c0.125,0.125,0.291,0.188,0.456,0.188c0.164,0,0.329-0.062,0.456-0.188l8.219-8.221c0.252-0.252,0.252-0.659,0-0.911c-0.252-0.252-0.659-0.252-0.911,0l-7.764,7.763L4.152,9.267c-0.252-0.251-0.66-0.251-0.911,0c-0.252,0.252-0.252,0.66,0,0.911L7.629,14.566z"
+ style="stroke: #009fdb; fill:#009fdb;"></path>
+ </svg>
+ <label>Default</label>
+ </div>
+ </div>
+ <div *ngIf="action.map.haveDefault" class="input-wrapper">
+ <input type="text" ngModel required name="defaultInput" data-tests-id="defaultInput" [(ngModel)]="action.map.default" class="input">
+ </div>
+ </div>
+
+ <table style="width: 100%; margin-bottom: 1rem;">
+ <thead style="background: #D2D2D2;">
+ <tr style="height: 30px;">
+ <th style="padding-left: 10px;">Key</th>
+ <th style="padding-left: 10px;">value</th>
+ </tr>
+ </thead>
+ <tbody ngModelGroup="mapKeyValue" #mapKeyValue="ngModelGroup">
+ <tr *ngFor="let item of action.map.values; let index = index;" (mouseleave)="hoveredIndex=-1" (mouseover)="hoveredIndex=index">
+ <th style="height: 30px; border: 1px solid #F3F3F3;">
+ <input [(ngModel)]="item.key" ngModel required name="mapValue[{{index}}]" data-tests-id="key" type="text" style="width:97%; height: 100%;border: none; padding:0 5px;">
+ </th>
+ <th style="height: 30px; border: 1px solid #F3F3F3;">
+ <input [(ngModel)]="item.value" ngModel required name="mapValue[{{index}}]" data-tests-id="value" type="text" style="width:97%; height: 100%;border: none; padding:0 5px;">
+ </th>
+ <th style="height: 30px; display: flex; align-items: baseline;">
+ <button mat-icon-button [ngStyle]="hoveredIndex === index ? {'opacity':'1'} : {'opacity':'0'}" class="button-remove" (click)="removeMapRow(index)"
+ *ngIf="action.map.values.length > 1" style="height: 24px; width: 24px; display:flex; box-shadow: none;">
+ <mat-icon class="md-24">delete</mat-icon>
+ </button>
+ </th>
+ </tr>
+ </tbody>
+ </table>
+
+
+ <div style="display:flex; justify-content: space-between;">
+ <div style="display: flex; align-items: center;">
+ <button mat-mini-fab color="primary" (click)="addMapRow()" style="height: 24px; width: 24px; display:flex; box-shadow: none;">
+ <mat-icon>add</mat-icon>
+ </button>
+ <span style="color: #009FDB; display: flex; justify-content: center; padding-left: 6px">Add Row</span>
+ </div>
+ </div>
+ </div>
+
+ </div>
+</form>
diff --git a/public/src/app/rule-engine/action/action.component.scss b/public/src/app/rule-engine/action/action.component.scss
new file mode 100644
index 0000000..f903db4
--- /dev/null
+++ b/public/src/app/rule-engine/action/action.component.scss
@@ -0,0 +1,116 @@
+.conatiner {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ justify-content: space-between;
+ margin: 10px 0;
+ .black {
+ color: black;
+ }
+ .highlight {
+ color: #009fdb;
+ }
+ .center-content {
+ display: flex;
+ width: 100%;
+ .action-info {
+ background: #f2f2f2;
+ padding: 6px 12px;
+ border-radius: 5px;
+ height: 32px;
+ margin: 0 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 142px;
+ }
+ }
+ .map-container {
+ padding-left: 115px;
+ .default {
+ display: flex;
+ width: 100%;
+ margin: 1rem 0;
+ min-height: 35px;
+ .input-wrapper {
+ width: 100%;
+ display: flex;
+ .input {
+ height: 20px;
+ padding: 5px;
+ margin-left: 10px;
+ width: 100%;
+ border: 1px solid #d2d2d2;
+ }
+ }
+ }
+ .grid-container {
+ padding-bottom: 10px;
+ .layout {
+ display: grid;
+ grid-template-columns: 1fr 1fr 30px;
+ grid-gap: 1px;
+ .title {
+ background-color: #f3f3f3;
+ height: 30px;
+ padding-left: 10px;
+ display: flex;
+ align-items: center;
+ }
+ .text-wrapper {
+ height: 30px;
+ border: 1px solid #f3f3f3;
+ .input {
+ width: 97%;
+ height: 100%;
+ border: none;
+ padding: 0 5px;
+ }
+ }
+ .btn-container {
+ height: 30px;
+ display: flex;
+ align-items: baseline;
+ }
+ }
+ }
+ }
+}
+
+.from {
+ display: flex;
+ flex-direction: column;
+ padding: 0 10px;
+ .from-conatiner {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ width: 100%;
+ min-width: 350px;
+ .input-text {
+ border: none;
+ flex: 1;
+ width: 100%;
+ min-width: 250px;
+ padding: 5px 0 5px 5px;
+ margin: 0;
+ }
+ }
+ .label {
+ border: 1px solid #d2d2d2;
+ height: 30px;
+ justify-content: flex-start;
+ align-items: center;
+ display: flex;
+ }
+}
+
+.button-remove {
+ display: flex;
+ justify-content: center;
+ color: #a7a7a7;
+ &:hover {
+ color: #009fdb;
+ }
+}
diff --git a/public/src/app/rule-engine/action/action.component.ts b/public/src/app/rule-engine/action/action.component.ts
new file mode 100644
index 0000000..9c7023f
--- /dev/null
+++ b/public/src/app/rule-engine/action/action.component.ts
@@ -0,0 +1,51 @@
+import { Component, Inject, Input, OnInit, ViewChild } from '@angular/core';
+// import { Copy } from "../model";
+import { Http, Response, Headers, RequestOptions } from '@angular/http';
+import { Observable } from 'rxjs/Rx';
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/catch';
+import { Subject } from 'rxjs/Subject';
+import { NgForm } from '@angular/forms';
+
+@Component({
+ selector: 'app-action',
+ templateUrl: './action.component.html',
+ styleUrls: ['./action.component.scss']
+})
+export class ActionComponent implements OnInit {
+ @Input() action;
+ @ViewChild('from') fromInstance;
+ @ViewChild('target') targetInstance;
+ @ViewChild('actionFrm') actionFrm: NgForm;
+ highlight = 'black';
+ hoveredIndex;
+ changeStyle($event) {
+ this.highlight = $event.type === 'mouseover' ? 'highlight' : 'black';
+ }
+ ngOnInit(): void {
+ console.log(this.action.id);
+ if (this.action.from !== '') {
+ console.log('Action %o', this.action);
+ this.fromInstance.updateMode(this.action.from);
+ this.targetInstance.updateMode(this.action);
+ }
+ }
+ updateFrom(data) {
+ this.action.from = data;
+ }
+ updateTarget(data) {
+ this.action.selectedNode = data;
+ }
+ /* map functionality */
+ addMapRow() {
+ this.action.map.values.push({ key: '', value: '' });
+ }
+ removeMapRow(index) {
+ this.action.map.values.splice(index, 1);
+ }
+
+ changeCheckbox() {
+ console.log(this.action.id);
+ return (this.action.map.haveDefault = !this.action.map.haveDefault);
+ }
+}
diff --git a/public/src/app/rule-engine/api/rule-engine-api.service.spec.ts b/public/src/app/rule-engine/api/rule-engine-api.service.spec.ts
new file mode 100644
index 0000000..e15535b
--- /dev/null
+++ b/public/src/app/rule-engine/api/rule-engine-api.service.spec.ts
@@ -0,0 +1,19 @@
+import { TestBed, inject } from '@angular/core/testing';
+import { HttpModule } from '@angular/http';
+import { RuleEngineApiService } from './rule-engine-api.service';
+
+describe('RuleEngineApiService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [HttpModule],
+ providers: [RuleEngineApiService]
+ });
+ });
+
+ it(
+ 'should be created',
+ inject([RuleEngineApiService], (service: RuleEngineApiService) => {
+ expect(service).toBeTruthy();
+ })
+ );
+});
diff --git a/public/src/app/rule-engine/api/rule-engine-api.service.ts b/public/src/app/rule-engine/api/rule-engine-api.service.ts
new file mode 100644
index 0000000..0d7ab5e
--- /dev/null
+++ b/public/src/app/rule-engine/api/rule-engine-api.service.ts
@@ -0,0 +1,134 @@
+import { Injectable, EventEmitter } from '@angular/core';
+import {
+ Http,
+ Response,
+ Headers,
+ RequestOptions,
+ URLSearchParams
+} from '@angular/http';
+import { Observable, Subject } from 'rxjs/Rx';
+// Import RxJs required methods
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/catch';
+import { environment } from '../../../environments/environment';
+import { v4 as uuid } from 'uuid';
+
+@Injectable()
+export class RuleEngineApiService {
+ options: RequestOptions;
+ headers: Headers;
+ baseUrl: string;
+ vfcmtUuid: string;
+ dcaeCompName: string;
+ nid: string;
+ configParam: string;
+ flowType: string;
+ editorData: Subject<any> = new Subject();
+ updateVersionLock: Subject<any> = new Subject();
+
+ constructor(private http: Http) {
+ this.baseUrl = `${environment.apiBaseUrl}/rule-editor`;
+ }
+
+ setParams(params) {
+ this.headers = new Headers({
+ 'Content-Type': 'application/json',
+ USER_ID: params.userId
+ });
+ this.options = new RequestOptions({ headers: this.headers });
+ this.vfcmtUuid = params.vfcmtUuid;
+ this.dcaeCompName = params.nodeName;
+ this.nid = params.nodeId;
+ this.configParam = params.fieldName;
+ this.flowType = params.flowType;
+ }
+
+ setFieldName(name) {
+ this.configParam = name;
+ }
+
+ getMetaData() {
+ const url = `${this.baseUrl}/list-events-by-versions`;
+ this.options.headers.set('X-ECOMP-RequestID', uuid());
+ return this.http
+ .get(url, this.options)
+ .map((res: Response) => res.json())
+ .catch((error: any) =>
+ Observable.throw(error.json().requestError || 'Server error')
+ );
+ }
+
+ getSchema(version, eventType) {
+ const url = `${this.baseUrl}/definition/${version}/${eventType}`;
+ this.options.headers.set('X-ECOMP-RequestID', uuid());
+ return this.http
+ .get(url, this.options)
+ .map((res: Response) => res.json())
+ .catch((error: any) =>
+ Observable.throw(error.json().requestError || 'Server error')
+ );
+ }
+
+ getListOfRules(): Observable<any> {
+ const url = `${this.baseUrl}/rule/${this.vfcmtUuid}/${this.dcaeCompName}/${
+ this.nid
+ }/${this.configParam}`;
+ this.options.headers.set('X-ECOMP-RequestID', uuid());
+ return this.http
+ .get(url, this.options)
+ .map(response => response.json())
+ .catch((error: any) => {
+ return Observable.throw(error.json().requestError || 'Server error');
+ });
+ }
+
+ modifyRule(newRole) {
+ const url = `${this.baseUrl}/rule/${this.vfcmtUuid}/${this.dcaeCompName}/${
+ this.nid
+ }/${this.configParam}`;
+ this.options.headers.set('X-ECOMP-RequestID', uuid());
+ return this.http
+ .post(url, newRole, this.options)
+ .map((res: Response) => res.json())
+ .catch((error: any) => {
+ return Observable.throw(error.json().requestError || 'Server error');
+ });
+ }
+
+ deleteRule(uid) {
+ const url = `${this.baseUrl}/rule/${this.vfcmtUuid}/${this.dcaeCompName}/${
+ this.nid
+ }/${this.configParam}/${uid}`;
+ this.options.headers.set('X-ECOMP-RequestID', uuid());
+ return this.http
+ .delete(url, this.options)
+ .map((res: Response) => res.json())
+ .catch((error: any) => {
+ return Observable.throw(error.json().requestError || 'Server error');
+ });
+ }
+
+ translate() {
+ const url = `${this.baseUrl}/rule/translate/${this.vfcmtUuid}/${
+ this.dcaeCompName
+ }/${this.nid}/${this.configParam}`;
+ this.options.headers.set('X-ECOMP-RequestID', uuid());
+ const params = new URLSearchParams();
+ params.append('flowType', this.flowType);
+ const options = { ...this.options, params: params };
+ return this.http
+ .get(url, options)
+ .map(response => response.json())
+ .catch((error: any) => {
+ return Observable.throw(error.json().requestError || 'Server error');
+ });
+ }
+
+ passDataToEditor(data) {
+ this.editorData.next(data);
+ }
+
+ callUpdateVersionLock() {
+ this.updateVersionLock.next();
+ }
+}
diff --git a/public/src/app/rule-engine/condition/condition.component.html b/public/src/app/rule-engine/condition/condition.component.html
new file mode 100644
index 0000000..a441f55
--- /dev/null
+++ b/public/src/app/rule-engine/condition/condition.component.html
@@ -0,0 +1,89 @@
+<tree-root #tree class="condition-tree" (initialized)="onInitialized(tree)" [nodes]="conditionTree" [options]="customTemplateStringOptions">
+ <ng-template #treeNodeTemplate let-node let-index="index">
+
+ <div>
+ <div *ngIf="node.data.name === 'operator'" style="background: #F2F2F2;">
+ <div style="display: flex; margin-left: 5px; align-items: center; min-height: 35px;">
+ <div style="display: flex; align-items: center;" *ngIf="showType">
+ <select style="padding: 5px;" [(ngModel)]="node.data.type">
+ <option value="ANY">ANY</option>
+ <option value="ALL">ALL</option>
+ </select>
+
+ <div style="display: flex; align-items: center; margin-left: 10px;">
+ of the following are true:
+ </div>
+ </div>
+
+ <div style="display: flex; margin-left: auto;">
+
+ <div style="display: flex; align-items: center; padding: 0 25px;">
+ <button mat-mini-fab color="primary" (click)="addConditional(tree, node)" style="height: 24px; width: 24px; display:flex; box-shadow: none;">
+ <mat-icon class="material-icons md-18">add</mat-icon>
+ </button>
+ <span class="btn-label">Add Condition
+ </span>
+ </div>
+
+ <div style="display: flex; align-items: center; padding: 0 25px;">
+ <button mat-mini-fab color="primary" data-tests-id="addConditionGroup" [disabled]="node.data.level === 2" (click)="addConditionalGroup(tree, node)"
+ style="height: 24px; width: 24px; display:flex; box-shadow: none;">
+ <mat-icon class="material-icons md-18">add</mat-icon>
+ </button>
+ <span [style.color]="node.data.level === 2 ? '#a7a7a7' : '#009fdb' " [style.cursor]="node.data.level === 2 ? 'default' : 'pointer' "
+ class="btn-label">Add Condition Group
+ </span>
+ </div>
+
+ <div style="display: flex; align-items: center; padding: 0 5px; background: #FFFFFF;">
+ <button mat-icon-button (click)="removeConditional(tree, node)" class="button-remove">
+ <mat-icon class="md-24">delete</mat-icon>
+ </button>
+ </div>
+
+ </div>
+ </div>
+ </div>
+ <div *ngIf="node.data.name === 'condition'">
+ <div class="from-conatiner" style="height:35px; ">
+ <div style="display: flex; width:90%;">
+ <div class="label" style="width:100%">
+ <span class="label" style="padding: 0 10px; border-left: none;">
+ Input
+ </span>
+ <input class="input-text" data-tests-id="left" [(ngModel)]="node.data.left" (ngModelChange)="modelChange($event)" ngDefaultControl
+ type="text">
+ </div>
+
+ <div style="margin: 0 1rem;">
+ <select style="height: 30px;" data-tests-id="selectOperator" [(ngModel)]="node.data.operator" (ngModelChange)="modelChange($event)"
+ ngDefaultControl>
+ <option [ngValue]="null" disabled>Select operator</option>
+ <option value="contains">Contains</option>
+ <option value="endsWith">Ends with</option>
+ <option value="startsWith">Starts with</option>
+ <option value="equals">Equals</option>
+ <option value="notEqual">Not equal</option>
+ </select>
+ </div>
+
+ <div class="label" style="width:100%">
+ <span class="label" style="padding: 0 10px; border-left: none;">
+ Value
+ </span>
+ <input class="input-text" data-tests-id="right" (ngModelChange)="modelChange($event)" [(ngModel)]="node.data.right" ngDefaultControl
+ type="text">
+ </div>
+ </div>
+ <!-- remove button -->
+ <div class="show-delete">
+ <button mat-icon-button (click)="removeConditional(tree, node)" class="button-remove">
+ <mat-icon class="md-24">delete</mat-icon>
+ </button>
+ </div>
+
+ </div>
+ </div>
+ </div>
+ </ng-template>
+</tree-root>
diff --git a/public/src/app/rule-engine/condition/condition.component.scss b/public/src/app/rule-engine/condition/condition.component.scss
new file mode 100644
index 0000000..8c0e9e0
--- /dev/null
+++ b/public/src/app/rule-engine/condition/condition.component.scss
@@ -0,0 +1,114 @@
+.condition-tree {
+ tree-viewport {
+ overflow-x: hidden;
+ overflow-y: hidden;
+ }
+ .angular-tree-component,
+ .tree-node-leaf {
+ margin: 0;
+ padding: 0;
+ }
+ .angular-tree-component {
+ padding-left: 1em;
+ overflow-y: hidden;
+ }
+ .tree-node-leaf.container {
+ border-bottom: 0px;
+ }
+ .tree-node-leaf.empty {
+ font-style: italic;
+ color: #fafafa;
+ border-color: #fafafa;
+ }
+ .tree-node-leaf div {
+ margin: 0;
+ top: 0.5em;
+ }
+ .node-wrapper {
+ background: white;
+ }
+ .tree-children {
+ border-left: 2px solid #f2f2f2;
+ // border-top: 1px solid #f2f2f2;
+ border-bottom: 1px solid #f2f2f2;
+ }
+ tree-node-expander {
+ display: none;
+ }
+ .node-content-wrapper {
+ padding-left: 0;
+ width: 100%;
+ .show-delete {
+ opacity: 0;
+ }
+ }
+ .tree-node-content {
+ width: 100%;
+ }
+ .node-content-wrapper-active,
+ .node-content-wrapper.node-content-wrapper-active:hover,
+ .node-content-wrapper-active.node-content-wrapper-focused {
+ background: white;
+ }
+ *:focus {
+ outline: none;
+ }
+
+ .node-content-wrapper-active,
+ .node-content-wrapper-focused,
+ .node-content-wrapper:hover {
+ box-shadow: none;
+ .show-delete {
+ opacity: 1;
+ display: flex;
+ align-items: center;
+ padding: 0 5px;
+ }
+ }
+}
+
+.from-conatiner {
+ display: flex;
+ align-items: center;
+ .input-text {
+ border: none;
+ flex: 1;
+ // width: 250px;
+ padding: 5px 0 5px 5px;
+ margin: 0;
+ }
+ .label {
+ border: 1px solid #d2d2d2;
+ height: 30px;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ cursor: default;
+ }
+}
+
+.btn-label {
+ display: flex;
+ justify-content: center;
+ padding-left: 5px;
+ color: #009fdb;
+}
+
+.button-label {
+ color: #a7a7a7;
+ display: flex;
+ justify-content: center;
+ padding-left: 5px;
+ &:hover {
+ color: #009fdb;
+ }
+}
+
+.button-remove {
+ display: flex;
+ justify-content: center;
+ color: #a7a7a7;
+ &:hover {
+ color: #009fdb;
+ }
+}
diff --git a/public/src/app/rule-engine/condition/condition.component.spec.ts b/public/src/app/rule-engine/condition/condition.component.spec.ts
new file mode 100644
index 0000000..bb0d38a
--- /dev/null
+++ b/public/src/app/rule-engine/condition/condition.component.spec.ts
@@ -0,0 +1,51 @@
+import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { HttpModule } from '@angular/http';
+import {
+ MatDialogModule,
+ MatButtonModule,
+ MatIconModule,
+ MatDialogRef,
+ MAT_DIALOG_DATA
+} from '@angular/material';
+
+import { ConditionComponent } from './condition.component';
+
+describe('Condition Component', () => {
+ let component: ConditionComponent;
+ let fixture: ComponentFixture<ConditionComponent>;
+ let de: DebugElement;
+ let el: HTMLElement;
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ HttpModule,
+ MatDialogModule,
+ MatButtonModule,
+ MatIconModule
+ ],
+ providers: [],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
+ declarations: [ConditionComponent]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ // create component and test fixture
+ fixture = TestBed.createComponent(ConditionComponent);
+ // get test component from the fixture
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/public/src/app/rule-engine/condition/condition.component.ts b/public/src/app/rule-engine/condition/condition.component.ts
new file mode 100644
index 0000000..f44fbf4
--- /dev/null
+++ b/public/src/app/rule-engine/condition/condition.component.ts
@@ -0,0 +1,161 @@
+import {
+ Component,
+ ViewEncapsulation,
+ ViewChild,
+ Output,
+ EventEmitter
+} from '@angular/core';
+import { TreeModel, TreeComponent, ITreeOptions } from 'angular-tree-component';
+import { some } from 'lodash';
+
+@Component({
+ selector: 'app-condition',
+ templateUrl: './condition.component.html',
+ styleUrls: ['./condition.component.scss'],
+ encapsulation: ViewEncapsulation.None
+})
+export class ConditionComponent {
+ conditionTree = [];
+ showType = false;
+ @ViewChild(TreeComponent) private tree: TreeComponent;
+ @Output() onConditionChange = new EventEmitter();
+ @Output() removeConditionCheck = new EventEmitter();
+ customTemplateStringOptions: ITreeOptions = {
+ isExpandedField: 'expanded',
+ animateExpand: true,
+ animateSpeed: 30,
+ animateAcceleration: 1.2
+ };
+
+ constructor() {
+ this.conditionTree.push({
+ name: 'operator',
+ level: 0,
+ type: 'ALL',
+ children: []
+ });
+ this.conditionTree[0].children.push({
+ name: 'condition',
+ left: '',
+ right: '',
+ operator: null,
+ level: 1
+ });
+ }
+
+ onInitialized(tree) {
+ tree.treeModel.expandAll();
+ }
+
+ updateMode(isSingle, data) {
+ if (isSingle) {
+ this.conditionTree[0].children.pop();
+ if (typeof data.right !== 'string') {
+ data.right = data.right.join(',');
+ }
+ this.conditionTree[0].children.push({
+ name: 'condition',
+ left: data.left,
+ right: data.right,
+ operator: data.operator,
+ level: 1
+ });
+ this.showType = false;
+ } else {
+ this.conditionTree = data;
+ setTimeout(() => (this.showType = true), 500);
+ }
+ this.tree.treeModel.update();
+ }
+
+ addConditional(tree, selectedNode) {
+ if (this.conditionTree[0].children.length > 0) {
+ this.showType = true;
+ }
+ const tempLevel =
+ selectedNode.data.name === 'condition'
+ ? selectedNode.data.level
+ : selectedNode.data.children[0].level;
+
+ const conditionTemplate = {
+ name: 'condition',
+ left: '',
+ right: '',
+ operator: null,
+ level: tempLevel
+ };
+ selectedNode.data.children.push(conditionTemplate);
+ tree.treeModel.update();
+ }
+
+ addConditionalGroup(tree, selectedNode) {
+ if (selectedNode.level < 3) {
+ if (this.conditionTree[0].children.length > 0) {
+ this.showType = true;
+ }
+ selectedNode.data.children.push({
+ name: 'operator',
+ level: selectedNode.data.level + 1,
+ type: 'ALL',
+ children: []
+ });
+
+ for (let i = 0; i < 2; i++) {
+ selectedNode.data.children[
+ selectedNode.data.children.length - 1
+ ].children.push({
+ name: 'condition',
+ left: '',
+ right: '',
+ operator: null,
+ level: selectedNode.data.level + 2
+ });
+ }
+ tree.treeModel.update();
+ tree.treeModel.expandAll();
+ }
+ }
+
+ removeConditional(tree, selectedNode) {
+ if (
+ (selectedNode.level === 1 && selectedNode.index === 0) ||
+ (selectedNode.parent.data.name === 'operator' &&
+ selectedNode.parent.level === 1 &&
+ selectedNode.parent.data.children.length === 1)
+ ) {
+ this.removeConditionCheck.emit(false);
+ } else if (
+ selectedNode.parent.level === 1 &&
+ selectedNode.parent.data.children.length === 2 &&
+ selectedNode.data.name === 'condition' &&
+ some(selectedNode.parent.data.children, { name: 'operator' })
+ ) {
+ return;
+ } else {
+ if (
+ selectedNode.parent.data.name === 'operator' &&
+ selectedNode.parent.level > 1
+ ) {
+ // Nested Group can delete when more then 2
+ if (selectedNode.parent.data.children.length > 2) {
+ this.deleteNodeAndUpdateTreeView(selectedNode, tree);
+ }
+ } else {
+ this.deleteNodeAndUpdateTreeView(selectedNode, tree);
+ if (this.conditionTree[0].children.length === 1) {
+ this.showType = false;
+ }
+ }
+ }
+ }
+
+ private deleteNodeAndUpdateTreeView(selectedNode: any, tree: any) {
+ selectedNode.parent.data.children.splice(selectedNode.index, 1);
+ tree.treeModel.update();
+ this.onConditionChange.emit(this.conditionTree);
+ }
+
+ modelChange(event) {
+ this.onConditionChange.emit(this.conditionTree);
+ }
+}
diff --git a/public/src/app/rule-engine/confirm-popup/confirm-popup.component.html b/public/src/app/rule-engine/confirm-popup/confirm-popup.component.html
new file mode 100644
index 0000000..49c800a
--- /dev/null
+++ b/public/src/app/rule-engine/confirm-popup/confirm-popup.component.html
@@ -0,0 +1,12 @@
+<div class="container" data-tests-id="delete-popup">
+ <div class="header">
+ Delete
+ </div>
+ <div class="content">
+ Are you sure you want to delete?
+ </div>
+ <div class="buttons">
+ <button mat-raised-button (click)="close(true)" data-tests-id="btnDelete" style="margin-right: 1rem;" color="primary">Delete</button>
+ <button mat-raised-button (click)="close(false)" data-tests-id="btnCancel" style="border: 1px solid #009FDB; color: #009FDB; background: #ffffff;">Cancel</button>
+ </div>
+</div>
diff --git a/public/src/app/rule-engine/confirm-popup/confirm-popup.component.scss b/public/src/app/rule-engine/confirm-popup/confirm-popup.component.scss
new file mode 100644
index 0000000..2a826ff
--- /dev/null
+++ b/public/src/app/rule-engine/confirm-popup/confirm-popup.component.scss
@@ -0,0 +1,20 @@
+.container {
+ display: flex;
+ justify-content: space-between;
+ margin: 0 !important;
+ border-top: solid 6px #ffb81c;
+ .header {
+ border-bottom: none;
+ }
+ .content {
+ margin: 1rem;
+ flex: 1;
+ font-weight: 400;
+ }
+ .buttons {
+ display: flex;
+ justify-content: flex-end;
+ border-top: solid 1px #eaeaea;
+ padding: 1rem;
+ }
+}
diff --git a/public/src/app/rule-engine/confirm-popup/confirm-popup.component.ts b/public/src/app/rule-engine/confirm-popup/confirm-popup.component.ts
new file mode 100644
index 0000000..23b6cee
--- /dev/null
+++ b/public/src/app/rule-engine/confirm-popup/confirm-popup.component.ts
@@ -0,0 +1,18 @@
+import { Component, Inject } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
+
+@Component({
+ selector: 'app-confirm-popup',
+ templateUrl: './confirm-popup.component.html',
+ styleUrls: ['./confirm-popup.component.scss']
+})
+export class ConfirmPopupComponent {
+ constructor(
+ public dialogRef: MatDialogRef<ConfirmPopupComponent>,
+ @Inject(MAT_DIALOG_DATA) public data: any
+ ) {}
+
+ close(flag) {
+ this.dialogRef.close(flag);
+ }
+}
diff --git a/public/src/app/rule-engine/from/from.component.html b/public/src/app/rule-engine/from/from.component.html
new file mode 100644
index 0000000..7af653d
--- /dev/null
+++ b/public/src/app/rule-engine/from/from.component.html
@@ -0,0 +1,70 @@
+<form #fromFrm="ngForm" novalidate>
+ <!-- Copy template -->
+ <div class="from" *ngIf="actionType === 'copy'" data-tests-id="fromComponent">
+ <div class="from-conatiner">
+ <div style="display: flex; align-items: center; width: 100%;" class="label">
+ <span class="label" style="padding: 0 5px; width: 50px;">From</span>
+ <input class="input-text" name="copyFrom" required style="min-width: 190px;" (ngModelChange)="modelChange(from)" #copyFrom="ngModel"
+ [(ngModel)]="from.value" type="text" data-tests-id="valueInput">
+ <span class="label" (click)="showRegex(from)" [ngStyle]="from.state === 'open' ? { 'color': '#009FDB'} : {'color':'gray'}"
+ style="padding: 0 5px; width: 50px; cursor: pointer; border: none" data-tests-id="btnFromRegex">Re/g</span>
+ </div>
+ <div [@state]="from.state" *ngIf="from.state === 'open'" style="display: flex; align-items: center; width: 80%;" class="label">
+ <span class="label" style="padding: 0 3px; width: 54px; border-top: none; border-bottom: none;">regex</span>
+ <input class="input-text" style="min-width: 192px;" (ngModelChange)="modelChange(from)" [(ngModel)]="from.regex" type="text"
+ ngModel required name="RegexInput" data-tests-id="inputFromRegex">
+ </div>
+ </div>
+ </div>
+ <!-- Map template -->
+ <div class="from" *ngIf="actionType === 'map'" data-tests-id="fromComponent">
+ <div class="from-conatiner">
+ <div style="display: flex; align-items: center; width: 100%;" class="label">
+ <span class="label" style="padding: 0 5px; width: 50px;">From</span>
+ <input class="input-text" ngModel required name="mapFromInput" (ngModelChange)="modelChange(from)" [(ngModel)]="from.value"
+ type="text" data-tests-id="valueInput">
+ </div>
+ </div>
+ </div>
+
+ <!-- dateFormatter template -->
+ <div class="from" *ngIf="actionType === 'date formatter'" data-tests-id="fromComponent">
+ <div class="from-conatiner">
+ <div style="display: flex; align-items: center; width: 100%;" class="label">
+ <span class="label" style="padding: 0 5px; width: 50px;">From</span>
+ <input class="input-text" ngModel required name="dateFormatterFromInput" (ngModelChange)="modelChange(from)" [(ngModel)]="from.value"
+ type="text" data-tests-id="valueInput">
+ </div>
+ </div>
+ </div>
+
+ <!-- Concat template -->
+ <div class="from" *ngIf="actionType === 'concat'" ngModelGroup="concat" #concatFrom="ngModelGroup">
+ <div *ngFor="let input of from.values; let index = index;" data-tests-id="concatInputArrayFrom" (mouseleave)="hoveredIndex=-1"
+ (mouseover)="hoveredIndex=index" class="from-conatiner" style="margin-bottom:1rem; display: flex; flex-direction: column; align-items: flex-start;"
+ data-tests-id="fromComponent">
+ <div style="display: flex; align-items: center; width: 100%;">
+ <div style="display: flex; align-items: center; width: 100%;" class="label">
+ <span class="label" style="padding: 0 5px; width: 50px;">From</span>
+ <input class="input-text" (ngModelChange)="modelChange(from)" [(ngModel)]="input.value" type="text" data-tests-id="valueInput"
+ ngModel required name="concat[{{index}}]">
+ </div>
+
+ <button mat-icon-button class="button-remove" [ngStyle]="hoveredIndex === index ? {'opacity':'1'} : {'opacity':'0'}" (click)="removeFromInput(index)"
+ *ngIf="from.values.length > 2" style="box-shadow: none; height: 24px; width: 24px; display:flex" data-tests-id="btnDelete">
+ <mat-icon class="md-24">delete</mat-icon>
+ </button>
+ </div>
+
+ </div>
+ <div style="display:flex; justify-content: space-between;">
+ <div style="display: flex; align-items: center;">
+ <button mat-mini-fab color="primary" (click)="addFromInput()" style="box-shadow: none; height: 24px; width: 24px; display:flex"
+ data-tests-id="btnAddInput">
+ <mat-icon>add</mat-icon>
+ </button>
+ <span style="color: #009FDB; display: flex; justify-content: center; padding-left: 6px">Add input</span>
+ </div>
+ </div>
+ </div>
+</form>
diff --git a/public/src/app/rule-engine/from/from.component.scss b/public/src/app/rule-engine/from/from.component.scss
new file mode 100644
index 0000000..852984d
--- /dev/null
+++ b/public/src/app/rule-engine/from/from.component.scss
@@ -0,0 +1,63 @@
+.from {
+ display: flex;
+ flex-direction: column;
+ padding: 0 10px;
+
+ .label {
+ border: 1px solid #d2d2d2;
+ height: 30px;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ }
+}
+
+.from-select {
+ width: 250px;
+ border: none;
+}
+
+.from-conatiner {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ width: 100%;
+ min-width: 350px;
+
+ .input-text {
+ border: none;
+ flex: 1;
+ width: 100%;
+ min-width: 250px;
+ padding: 5px 0 5px 5px;
+ margin: 0;
+ }
+}
+
+.button-remove {
+ display: flex;
+ justify-content: center;
+ color: #a7a7a7;
+ &:hover {
+ color: #009fdb;
+ }
+}
+
+:host {
+ @mixin md-icon-size($size: 24px) {
+ font-size: $size;
+ height: $size;
+ width: $size;
+ }
+
+ .material-icons.mat-icon {
+ @include md-icon-size(24px);
+ }
+ /deep/ .mat-button-wrapper {
+ padding: 0;
+ }
+ .mat-icon {
+ width: 18px;
+ height: 18px;
+ }
+}
diff --git a/public/src/app/rule-engine/from/from.component.ts b/public/src/app/rule-engine/from/from.component.ts
new file mode 100644
index 0000000..e7c276b
--- /dev/null
+++ b/public/src/app/rule-engine/from/from.component.ts
@@ -0,0 +1,91 @@
+import {
+ Component,
+ Input,
+ Output,
+ EventEmitter,
+ ViewChild
+} from '@angular/core';
+// import { From } from "../model";
+import { Subject } from 'rxjs/Subject';
+import {
+ trigger,
+ state,
+ animate,
+ transition,
+ style,
+ keyframes
+} from '@angular/animations';
+import { NgForm } from '@angular/forms';
+
+@Component({
+ selector: 'app-from',
+ templateUrl: './from.component.html',
+ styleUrls: ['./from.component.scss'],
+ animations: [
+ trigger('state', [
+ state(
+ 'open',
+ style({
+ opacity: 1,
+ height: 'auto'
+ })
+ ),
+ transition('* => open', [
+ animate(
+ 200,
+ keyframes([
+ style({
+ opacity: 1,
+ height: 'auto'
+ })
+ ])
+ )
+ ]),
+ state(
+ 'closed',
+ style({
+ opacity: 0,
+ height: 0
+ })
+ )
+ ])
+ ]
+})
+export class FromComponent {
+ from: any = {
+ value: '',
+ regex: '',
+ state: 'closed',
+ values: [{ value: '' }, { value: '' }]
+ };
+ @Input() actionType;
+ @Output() onFromChange = new EventEmitter();
+ @ViewChild('fromFrm') fromFrm: NgForm;
+ hoveredIndex;
+ // public keyUp = new BehaviorSubject<string>(null);
+
+ showRegex(item) {
+ item.state = item.state === 'closed' ? 'open' : 'closed';
+ if (item.state === 'closed') {
+ item.regex = '';
+ }
+ }
+ updateMode(fromData) {
+ console.log(fromData);
+ if (fromData) {
+ this.from = fromData;
+ }
+ }
+
+ constructor() {}
+
+ modelChange(event) {
+ this.onFromChange.emit(event);
+ }
+ addFromInput() {
+ this.from.values.push({ value: '' });
+ }
+ removeFromInput(index) {
+ this.from.values.splice(index, 1);
+ }
+}
diff --git a/public/src/app/rule-engine/host/exit-mode.enum.ts b/public/src/app/rule-engine/host/exit-mode.enum.ts
new file mode 100644
index 0000000..784ba3b
--- /dev/null
+++ b/public/src/app/rule-engine/host/exit-mode.enum.ts
@@ -0,0 +1,4 @@
+export enum ExitMode {
+ Done,
+ Cancel
+}
diff --git a/public/src/app/rule-engine/host/host-params.ts b/public/src/app/rule-engine/host/host-params.ts
new file mode 100644
index 0000000..f204101
--- /dev/null
+++ b/public/src/app/rule-engine/host/host-params.ts
@@ -0,0 +1,8 @@
+export interface HostParams {
+ readonly vfcmtUuid: string;
+ readonly nodeName: string;
+ readonly nodeId: string;
+ readonly fieldName: string;
+ readonly userId: string;
+ readonly flowType: string;
+}
diff --git a/public/src/app/rule-engine/host/host.service.spec.ts b/public/src/app/rule-engine/host/host.service.spec.ts
new file mode 100644
index 0000000..048be80
--- /dev/null
+++ b/public/src/app/rule-engine/host/host.service.spec.ts
@@ -0,0 +1,18 @@
+import { TestBed, inject } from '@angular/core/testing';
+
+import { HostService } from './host.service';
+
+describe('HostService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [HostService]
+ });
+ });
+
+ it(
+ 'should be created',
+ inject([HostService], (service: HostService) => {
+ expect(service).toBeTruthy();
+ })
+ );
+});
diff --git a/public/src/app/rule-engine/host/host.service.ts b/public/src/app/rule-engine/host/host.service.ts
new file mode 100644
index 0000000..7918d30
--- /dev/null
+++ b/public/src/app/rule-engine/host/host.service.ts
@@ -0,0 +1,56 @@
+import { Injectable } from '@angular/core';
+import { HostParams } from './host-params';
+import { ExitMode } from './exit-mode.enum';
+
+@Injectable()
+export class HostService {
+ /* Public Members */
+
+ public static getParams(): HostParams {
+ return this.getQueryParamsObj(window.location.search) as HostParams;
+ }
+
+ public static enterModifyRule(): void {
+ this.postMessage('modifyRule', null);
+ }
+
+ public static exitModifyRule(): void {
+ this.postMessage('ruleList', null);
+ }
+
+ public static disableLoader(): void {
+ this.postMessage('disable-loader', null);
+ }
+
+ public static exit(mode: ExitMode, data: string): void {
+ if (mode === ExitMode.Cancel) {
+ this.postMessage('exit', null);
+ } else if (mode === ExitMode.Done) {
+ this.postMessage('exit', data);
+ }
+ }
+
+ /* Private Methods */
+
+ private static postMessage(eventName: string, data: string): void {
+ window.parent.postMessage(
+ {
+ type: eventName,
+ data: data
+ },
+ '*'
+ );
+ }
+
+ private static getQueryParamsObj(query: string): object {
+ return query
+ .substring(1) // removes '?' that always appears as prefix to the query-string
+ .split('&') // splits query-string to "key=value" strings
+ .map(p => p.split('=')) // splits each "key=value" string to [key,value] array
+ .reduce((res, p) => {
+ // converts to a dictionary (object) of params
+ res[p[0]] = p[1];
+ return res;
+ }, {});
+ }
+}
diff --git a/public/src/app/rule-engine/rule-list/rule-list.component.html b/public/src/app/rule-engine/rule-list/rule-list.component.html
new file mode 100644
index 0000000..c68c706
--- /dev/null
+++ b/public/src/app/rule-engine/rule-list/rule-list.component.html
@@ -0,0 +1,73 @@
+<div class="container">
+ <div class="header">
+ <span style="font-size: 18px;">Rule Engine</span>
+ <div style="display:flex">
+ <button mat-raised-button (click)="translateRules()" color="primary" [disabled]="store.ruleList.length === 0" style="margin-left: 20px;"
+ data-tests-id="btnTranslate">
+ Translate
+ </button>
+ <app-bar-icons [tabName]="this.store.tabParmasForRule[0].name"></app-bar-icons>
+ </div>
+ </div>
+
+ <div style="margin: 0rem 1rem; flex-grow: 1; overflow-y: auto;">
+
+ <!-- error container -->
+ <div *ngIf="error" style="color: white; background: red; padding: 1rem; border-radius: 5px; font-weight: bold;">
+ {{ error }}
+ </div>
+
+ <app-version-type-select #versionEventType [versions]="versions" [metaData]="metaData" (nodesUpdated)="handleUpdateNode($event)"
+ (refrashRuleList)="handlePropertyChange()"></app-version-type-select>
+
+ <div *ngIf="targetSource && store.ruleList.length === 0" style="margin: 30px 0; display: flex; align-items: center; justify-content: center; flex-direction: column;">
+
+ <div style="margin: 3em 0 2em 0;">
+ <div style="font-size: 1.5em;">
+ Rules were not yet created
+ </div>
+ <div style="padding: 0.5em; padding-top: 1em;">
+ Please create a new normalization rule
+ </div>
+ </div>
+
+ <button mat-fab (click)="openAction()" style="background-color:#009FDB" data-tests-id="btnAddFirstRule">
+ <span [innerHTML]="'plus' | feather:24"></span>
+ </button>
+ <span style="margin-top: 1rem; font-size: 14px; color: #009FDB;">
+ Add First Rule
+ </span>
+ </div>
+
+ <div *ngIf="store.ruleList.length > 0">
+ <div style="padding: 10px 0;">
+ Rules
+ </div>
+ <div style="display: flex; align-items: center;">
+ <button mat-mini-fab color="primary" id="addMoreRule" data-tests-id="addMoreRule" style="height: 24px; width: 24px; display:flex"
+ (click)="openAction()">
+ <mat-icon class="material-icons md-18">add</mat-icon>
+ </button>
+ <span style="color: #009FDB; display: flex; justify-content: center; padding-left: 10px">Add Rule</span>
+ </div>
+ </div>
+
+ <div style="margin: 30px 0 10px 0;">
+
+ <div *ngFor="let item of store.ruleList; let index = index" data-tests-id="ruleElement" (mouseleave)="hoveredIndex=-1" (mouseover)="hoveredIndex=index"
+ class="item" style="display: flex;" [ngStyle]="hoveredIndex === index ? {'background-color': '#E6F6FB', 'color': '#009FDB'} : {'background-color': '#FFFFFF', 'color':'gray'}">
+ <span style="width:100%; display: flex; align-items: center;">
+ {{item.description}} - [{{item.uid}}]
+ </span>
+ <div style="display: flex; justify-content: flex-end;" *ngIf="index==hoveredIndex">
+ <button (click)="openAction(item)" data-tests-id="editRule" class="btn-list" mat-icon-button>
+ <mat-icon class="md-24">mode_edit</mat-icon>
+ </button>
+ <button (click)="removeItem(item.uid)" data-tests-id="deleteRule" class="btn-list" mat-icon-button>
+ <mat-icon class="md-24">delete</mat-icon>
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/public/src/app/rule-engine/rule-list/rule-list.component.scss b/public/src/app/rule-engine/rule-list/rule-list.component.scss
new file mode 100644
index 0000000..c4aee05
--- /dev/null
+++ b/public/src/app/rule-engine/rule-list/rule-list.component.scss
@@ -0,0 +1,109 @@
+.container {
+ // margin: 1rem;
+ position: relative;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ .header {
+ position: relative;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ color: #191919;
+ border-bottom: 2px solid #d2d2d2;
+ padding-bottom: 0.5rem;
+ margin: 1rem;
+ }
+
+ .item {
+ border: 1px solid #d2d2d2;
+ padding: 0 10px;
+ height: 40px;
+ }
+
+ .mat-fab,
+ .mat-mini-fab,
+ .mat-raised-button {
+ box-shadow: none;
+ }
+}
+.my-full-screen-dialog .mat-dialog-container {
+ max-width: none;
+ width: 100vw;
+ height: 100vh;
+ padding: 0;
+}
+
+.my-confrim-dialog .mat-dialog-container {
+ max-width: 600px;
+ width: 500px;
+ height: 200px;
+ padding: 0;
+}
+
+.btn-list {
+ display: flex !important;
+ justify-content: center !important;
+ color: #d2d2d2 !important;
+
+ &:hover {
+ color: #009fdb !important;
+ }
+}
+
+.hr {
+ display: block;
+ margin: 10px 0 10px 0;
+ border-top: 1px solid rgba(0, 0, 0, 0.12);
+ width: 100%;
+}
+
+.mat-fab,
+.mat-mini-fab,
+.mat-raised-button {
+ box-shadow: none;
+}
+
+.mat-mini-fab .mat-button-wrapper {
+ padding: 0 !important;
+}
+.mat-icon {
+ // width: 18px;
+ // height: 18px;
+ display: flex !important;
+ justify-content: center !important;
+ align-items: center !important;
+}
+/* Rules for sizing the icon. */
+.material-icons.md-18 {
+ font-size: 18px;
+}
+.material-icons.md-24 {
+ font-size: 24px;
+}
+.material-icons.md-30 {
+ font-size: 30px;
+}
+.material-icons.md-36 {
+ font-size: 36px;
+}
+.material-icons.md-48 {
+ font-size: 48px;
+}
+
+/* Rules for using icons as black on a light background. */
+.material-icons.md-dark {
+ color: rgba(0, 0, 0, 0.54);
+}
+.material-icons.md-dark.md-inactive {
+ color: rgba(0, 0, 0, 0.26);
+}
+
+/* Rules for using icons as white on a dark background. */
+.material-icons.md-light {
+ color: rgba(255, 255, 255, 1);
+}
+.material-icons.md-light.md-inactive {
+ color: rgba(255, 255, 255, 0.3);
+}
diff --git a/public/src/app/rule-engine/rule-list/rule-list.component.ts b/public/src/app/rule-engine/rule-list/rule-list.component.ts
new file mode 100644
index 0000000..45cfbd0
--- /dev/null
+++ b/public/src/app/rule-engine/rule-list/rule-list.component.ts
@@ -0,0 +1,197 @@
+import { Component, ViewEncapsulation, ViewChild } from '@angular/core';
+import { MatDialog } from '@angular/material';
+import { ActionListComponent } from '../action-list/action-list.component';
+import { RuleEngineApiService } from '../api/rule-engine-api.service';
+import { ConfirmPopupComponent } from '../confirm-popup/confirm-popup.component';
+import { Store } from '../../store/store';
+import { isEmpty } from 'lodash';
+import { ToastrService } from 'ngx-toastr';
+import { timer } from 'rxjs/observable/timer';
+
+const primaryColor = '#009fdb';
+
+@Component({
+ selector: 'app-rule-list',
+ templateUrl: './rule-list.component.html',
+ styleUrls: ['./rule-list.component.scss'],
+ encapsulation: ViewEncapsulation.None
+})
+export class RuleListComponent {
+ @ViewChild('versionEventType') versionType;
+ error: Array<string>;
+ // list = new Array();
+ schema;
+ targetSource;
+ dialogRef;
+ crud;
+ hoveredIndex;
+ params;
+ versions;
+ metaData;
+
+ private errorHandler(error: any) {
+ this.store.loader = false;
+ console.log(error);
+ this.error = [];
+ if (typeof error === 'string') {
+ this.error.push(error);
+ } else {
+ console.log(error.notes);
+ const errorFromServer = Object.values(error)[0] as any;
+ if (Object.keys(error)[0] === 'serviceExceptions') {
+ this.error = errorFromServer.map(x => x.formattedErrorMessage);
+ } else {
+ this.error = errorFromServer.formattedErrorMessage;
+ }
+ }
+ }
+
+ private getListOfRules() {
+ this._ruleApi.getListOfRules().subscribe(
+ response => {
+ console.log('res: %o', response);
+ if (response && Object.keys(response).length !== 0) {
+ this.versionType.updateData(
+ response.version,
+ response.eventType,
+ true
+ );
+ this.store.updateRuleList(Object.values(response.rules));
+ this.targetSource = response.schema;
+ } else {
+ this.store.resetRuleList();
+ this.versionType.updateVersionTypeFlag(false);
+ this.targetSource = null;
+ // if the the list is empty then get version and domain events
+ this._ruleApi.getMetaData().subscribe(data => {
+ console.log(data);
+ this.versions = data.map(x => x.version);
+ this.metaData = data;
+ });
+ }
+ this.store.loader = false;
+ },
+ error => {
+ this.errorHandler(error);
+ }
+ );
+ }
+
+ constructor(
+ private _ruleApi: RuleEngineApiService,
+ public dialog: MatDialog,
+ private toastr: ToastrService,
+ public store: Store
+ ) {
+ this.store.loader = true;
+ this.params = {
+ vfcmtUuid: this.store.mcUuid,
+ nodeName: this.store.tabParmasForRule[0].name,
+ nodeId: this.store.tabParmasForRule[0].nid,
+ fieldName: this.store.configurationForm[0].name,
+ userId: 'ym903w', // this.store.sdcParmas.userId
+ flowType: this.store.cdump.flowType
+ };
+ console.log('params: %o', this.params);
+ this.store.loader = true;
+ // set api params by iframe url query
+ this._ruleApi.setParams(this.params);
+ this.getListOfRules();
+ }
+
+ handlePropertyChange() {
+ this.store.loader = true;
+ this.error = null;
+ this.getListOfRules();
+ }
+
+ translateRules() {
+ this.store.loader = true;
+ // send translate JSON
+ this._ruleApi.translate().subscribe(
+ data => {
+ this.store.loader = false;
+ console.log(JSON.stringify(data));
+ let domElementName: string;
+ this.store.configurationForm.forEach(property => {
+ console.log('mappingTarget ', this.versionType.mappingTarget);
+ if (property.name === this.versionType.mappingTarget) {
+ property.assignment.value = JSON.stringify(data);
+ domElementName = property.name;
+ console.log(property.name);
+ }
+ });
+ this.toastr.success('', 'Translate succeeded');
+ this.store.expandAdvancedSetting[this.store.tabIndex] = true;
+ const source = timer(500);
+ source.subscribe(val => {
+ const el = document.getElementById(domElementName);
+ const label = el.children.item(0) as HTMLElement;
+ label.style.color = primaryColor;
+ const input = el.children.item(1) as HTMLElement;
+ input.style.color = primaryColor;
+ input.style.borderColor = primaryColor;
+ el.scrollIntoView();
+ });
+ },
+ error => {
+ this.errorHandler(error);
+ }
+ );
+ }
+
+ handleUpdateNode(data) {
+ this.targetSource = data.nodes;
+ this.store.resetRuleList();
+ }
+
+ removeItem(uid) {
+ this.dialogRef = this.dialog.open(ConfirmPopupComponent, {
+ panelClass: 'my-confrim-dialog',
+ disableClose: true
+ });
+ this.dialogRef.afterClosed().subscribe(result => {
+ // if the user want to delete
+ if (result) {
+ // call be api
+ this.store.loader = true;
+ this._ruleApi.deleteRule(uid).subscribe(
+ success => {
+ this.store.removeRuleFromList(uid);
+ // if its the last rule
+ if (this.store.ruleList.length === 0) {
+ this._ruleApi.getMetaData().subscribe(data => {
+ console.log(data);
+ this.versions = data.map(x => x.version);
+ this.metaData = data;
+ this.versionType.updateVersionTypeFlag(false);
+ this.targetSource = null;
+ });
+ }
+ this.store.loader = false;
+ },
+ error => {
+ this.store.loader = false;
+ this.errorHandler(error);
+ }
+ );
+ }
+ });
+ }
+
+ openAction(item): void {
+ this.crud = isEmpty(item) ? 'new' : 'edit';
+ this._ruleApi.passDataToEditor({
+ version: this.versionType.selectedVersion,
+ eventType: this.versionType.selectedEvent,
+ targetSource: this.targetSource,
+ item: isEmpty(item) ? null : item,
+ params: this.params
+ });
+ this.store.isLeftVisible = false;
+
+ this._ruleApi.updateVersionLock.subscribe(() => {
+ this.versionType.updateVersionTypeFlag(true);
+ });
+ }
+}
diff --git a/public/src/app/rule-engine/slide-panel/slide-panel.component.html b/public/src/app/rule-engine/slide-panel/slide-panel.component.html
new file mode 100644
index 0000000..f0ee27e
--- /dev/null
+++ b/public/src/app/rule-engine/slide-panel/slide-panel.component.html
@@ -0,0 +1,8 @@
+<div class="panes" [@slide]="activePane">
+ <div style="height: 100%">
+ <ng-content select="[leftPane]"></ng-content>
+ </div>
+ <div style="height: 100%">
+ <ng-content select="[rightPane]"></ng-content>
+ </div>
+</div>
diff --git a/public/src/app/rule-engine/slide-panel/slide-panel.component.scss b/public/src/app/rule-engine/slide-panel/slide-panel.component.scss
new file mode 100644
index 0000000..2c9f00a
--- /dev/null
+++ b/public/src/app/rule-engine/slide-panel/slide-panel.component.scss
@@ -0,0 +1,15 @@
+:host {
+ display: block;
+ overflow: hidden;
+ height: 100%;
+}
+
+.panes {
+ height: 100%;
+ width: 200%;
+
+ display: flex;
+ div {
+ flex: 1;
+ }
+}
diff --git a/public/src/app/rule-engine/slide-panel/slide-panel.component.ts b/public/src/app/rule-engine/slide-panel/slide-panel.component.ts
new file mode 100644
index 0000000..d7aa652
--- /dev/null
+++ b/public/src/app/rule-engine/slide-panel/slide-panel.component.ts
@@ -0,0 +1,27 @@
+import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
+import {
+ animate,
+ state,
+ style,
+ transition,
+ trigger
+} from '@angular/animations';
+
+type PaneType = 'left' | 'right';
+
+@Component({
+ selector: 'app-slide-panel',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ templateUrl: './slide-panel.component.html',
+ styleUrls: ['./slide-panel.component.scss'],
+ animations: [
+ trigger('slide', [
+ state('left', style({ transform: 'translateX(0)' })),
+ state('right', style({ transform: 'translateX(-50%)' })),
+ transition('* => *', animate(300))
+ ])
+ ]
+})
+export class SlidePanelComponent {
+ @Input() activePane: PaneType = 'left';
+}
diff --git a/public/src/app/rule-engine/target/target.component.html b/public/src/app/rule-engine/target/target.component.html
new file mode 100644
index 0000000..7a321ef
--- /dev/null
+++ b/public/src/app/rule-engine/target/target.component.html
@@ -0,0 +1,28 @@
+<form #targetFrm="ngForm" novalidate class="target">
+ <div class="top-select">
+ <span class="label" style="border-right: none;">Target</span>
+ <input class="text-input" style="border-right: none;" type="text" [(ngModel)]="selectedNode.id" (ngModelChange)="inputChange()"
+ ngModel required name="targetInput" data-tests-id="inputTarget">
+ <span class="label clickable" data-tests-id="openTargetTree" style="border-left: none;" (click)="showOption = !showOption">
+ <img src="{{imgBase}}/target.svg" alt="target">
+ </span>
+ </div>
+ <div class="bottom-select" *ngIf="showOption" [@toggleDropdown]>
+ <div class="filter-container" style="display: flex; border-bottom: 1px solid #F2F2F2;margin-bottom: 1rem; width:100%;">
+ <input id="filter" #filter class="filter" (keyup)="tree.treeModel.filterNodes(filter.value)" placeholder="Search..." />
+ <button mat-raised-button style="min-width: 18px; box-shadow: none; display: flex; justify-content: center;" (click)="tree.treeModel.clearFilter(); filter.value = ''">
+ <mat-icon>clear</mat-icon>
+ </button>
+ </div>
+
+ <tree-root #tree [focused]="true" class="targetTree" (event)="onEvent($event)" [nodes]="nodes" [options]="options">
+ <ng-template #treeNodeTemplate let-node let-index="index">
+ <span *ngIf="node.data.isRequired" class="required"></span>
+ <span data-tests-id="targetNode">
+ {{ node.data.name }}
+ </span>
+ </ng-template>
+ </tree-root>
+
+ </div>
+</form>
diff --git a/public/src/app/rule-engine/target/target.component.scss b/public/src/app/rule-engine/target/target.component.scss
new file mode 100644
index 0000000..ed2d70e
--- /dev/null
+++ b/public/src/app/rule-engine/target/target.component.scss
@@ -0,0 +1,99 @@
+.targetTree {
+ tree-viewport {
+ overflow: hidden;
+ }
+}
+
+.conatiner {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ justify-content: space-between;
+
+ .center-content {
+ display: flex;
+ width: 100%;
+
+ .action-info {
+ background: #93cdff;
+ padding: 6px;
+ border-radius: 5px;
+ height: 20px;
+ margin: 0 10px;
+ }
+
+ .regex {
+ max-width: 250px;
+ float: right;
+ display: flex;
+ align-items: center;
+ padding: 20px 10px;
+
+ .label {
+ border: 1px solid #d2d2d2;
+ padding: 0 5px;
+ height: 30px;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ }
+ }
+ }
+}
+.target {
+ width: 100%;
+ .top-select {
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ }
+ .label {
+ border: 1px solid #d2d2d2;
+ padding: 0 5px;
+ height: 30px;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ }
+
+ .bottom-select {
+ border: 1px solid #ccc;
+ padding: 7px;
+ .filter-container {
+ padding: 5px;
+ .filter {
+ background: #fff;
+ color: black;
+ font: inherit;
+ border: 0;
+ outline: 0;
+ padding: 10px;
+ width: 100%;
+ }
+ }
+ }
+}
+
+.small-padding {
+ padding-right: 10px;
+}
+
+.text-input {
+ width: 100%;
+ height: 30px;
+ margin: 0;
+ padding: 0 5px;
+ border: 1px solid #d2d2d2;
+}
+
+.clickable {
+ cursor: pointer;
+}
+
+.required::before {
+ content: '*';
+ color: red;
+}
diff --git a/public/src/app/rule-engine/target/target.component.spec.ts b/public/src/app/rule-engine/target/target.component.spec.ts
new file mode 100644
index 0000000..6ddd8cd
--- /dev/null
+++ b/public/src/app/rule-engine/target/target.component.spec.ts
@@ -0,0 +1,57 @@
+import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { MatButtonModule, MatIconModule } from '@angular/material';
+// component
+import { TargetComponent } from './target.component';
+
+describe('TargetComponent', () => {
+ let component: TargetComponent;
+ let fixture: ComponentFixture<TargetComponent>;
+ let de: DebugElement;
+ let el: HTMLElement;
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ BrowserAnimationsModule,
+ MatButtonModule,
+ MatIconModule
+ ],
+ providers: [],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
+ declarations: [TargetComponent]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ // create component and test fixture
+ fixture = TestBed.createComponent(TargetComponent);
+ // get test component from the fixture
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should open target tree when click on button', () => {
+ const openTargetElement = fixture.debugElement
+ .query(By.css('span[data-tests-id=openTargetTree]'))
+ .nativeElement.click();
+
+ fixture.detectChanges();
+
+ const treeContainer = fixture.debugElement.query(
+ By.css('.filter-container')
+ );
+ expect(treeContainer).not.toBeNull();
+ });
+});
diff --git a/public/src/app/rule-engine/target/target.component.ts b/public/src/app/rule-engine/target/target.component.ts
new file mode 100644
index 0000000..f17cdef
--- /dev/null
+++ b/public/src/app/rule-engine/target/target.component.ts
@@ -0,0 +1,77 @@
+import {
+ Component,
+ ViewEncapsulation,
+ ViewChild,
+ Input,
+ Output,
+ EventEmitter
+} from '@angular/core';
+import { TreeModel, TreeComponent, ITreeOptions } from 'angular-tree-component';
+import {
+ trigger,
+ state,
+ animate,
+ transition,
+ style
+} from '@angular/animations';
+import { fuzzysearch, getBranchRequierds, validation } from './target.util';
+import { environment } from '../../../environments/environment';
+import { NgForm } from '@angular/forms';
+
+@Component({
+ selector: 'app-target',
+ templateUrl: './target.component.html',
+ styleUrls: ['./target.component.scss'],
+ encapsulation: ViewEncapsulation.None,
+ animations: [
+ trigger('toggleDropdown', [
+ transition('void => *', [
+ style({ opacity: 0, offset: 0, height: 0 }),
+ animate('300ms cubic-bezier(0.17, 0.04, 0.03, 0.94)')
+ ]),
+ transition('* => void', [
+ style({ opacity: 1, offset: 1, height: 'auto' }),
+ animate('100ms cubic-bezier(0.17, 0.04, 0.03, 0.94)')
+ ])
+ ])
+ ]
+})
+export class TargetComponent {
+ imgBase = environment.imagePath;
+ showOption = false;
+ selectedNode = { name: '', id: '' };
+ @Input() nodes;
+ @Output() onTargetChange = new EventEmitter();
+ @ViewChild(TreeComponent) private tree: TreeComponent;
+ @ViewChild('targetFrm') targetFrm: NgForm;
+ options: ITreeOptions = {
+ animateExpand: true,
+ animateSpeed: 30,
+ animateAcceleration: 1.2
+ };
+
+ filterFn(value, treeModel: TreeModel) {
+ treeModel.filterNodes(node => fuzzysearch(value, node.data.name));
+ }
+
+ inputChange() {
+ this.onTargetChange.emit(this.selectedNode.id);
+ }
+
+ updateMode(action) {
+ this.selectedNode = {
+ id: action.target,
+ name: ''
+ };
+ }
+
+ onEvent(event) {
+ if (event.eventName === 'activate') {
+ if (event.node.data.children === null) {
+ this.selectedNode = event.node.data;
+ this.onTargetChange.emit(this.selectedNode);
+ this.showOption = false;
+ }
+ }
+ }
+}
diff --git a/public/src/app/rule-engine/target/target.util.ts b/public/src/app/rule-engine/target/target.util.ts
new file mode 100644
index 0000000..6a6df62
--- /dev/null
+++ b/public/src/app/rule-engine/target/target.util.ts
@@ -0,0 +1,50 @@
+export function getBranchRequierds(node, requiredArr) {
+ if (node.parent) {
+ if (node.parent.data.hasOwnProperty('requiredChildren')) {
+ requiredArr.push(node.parent.data.requiredChildren);
+ }
+ return getBranchRequierds(node.parent, requiredArr);
+ }
+ return requiredArr;
+}
+
+export function validation(node, userSelection) {
+ const requiredArr = [];
+ const validationRequired = getBranchRequierds(node, requiredArr);
+ const nonValidationArr = [];
+ validationRequired.forEach(nodeRequireds => {
+ return nodeRequireds.forEach(levelRequired => {
+ if (userSelection.filter(node => node === levelRequired).length === 0) {
+ nonValidationArr.push(levelRequired);
+ }
+ return;
+ });
+ });
+ return nonValidationArr;
+}
+
+export function fuzzysearch(needle, haystack) {
+ const haystackLC = haystack.toLowerCase();
+ const needleLC = needle.toLowerCase();
+
+ const hlen = haystack.length;
+ const nlen = needleLC.length;
+
+ if (nlen > hlen) {
+ return false;
+ }
+ if (nlen === hlen) {
+ return needleLC === haystackLC;
+ }
+ outer: for (let i = 0, j = 0; i < nlen; i++) {
+ const nch = needleLC.charCodeAt(i);
+
+ while (j < hlen) {
+ if (haystackLC.charCodeAt(j++) === nch) {
+ continue outer;
+ }
+ }
+ return false;
+ }
+ return true;
+}
diff --git a/public/src/app/rule-engine/target/target.validation.spec.ts b/public/src/app/rule-engine/target/target.validation.spec.ts
new file mode 100644
index 0000000..71dc083
--- /dev/null
+++ b/public/src/app/rule-engine/target/target.validation.spec.ts
@@ -0,0 +1,83 @@
+import { TestBed, async } from '@angular/core/testing';
+import { TreeModel, TreeComponent, ITreeOptions } from 'angular-tree-component';
+import { validation, getBranchRequierds } from './target.util';
+
+const _nodes = [
+ {
+ id: 1,
+ name: 'North America',
+ requiredChildren: ['United States'],
+ children: [
+ {
+ id: 11,
+ name: 'United States',
+ requiredChildren: ['New York', 'Florida'],
+ children: [
+ { id: 111, name: 'New York' },
+ { id: 112, name: 'California' },
+ { id: 113, name: 'Florida' }
+ ]
+ },
+ { id: 12, name: 'Canada' }
+ ]
+ },
+ {
+ name: 'South America',
+ children: [{ name: 'Argentina', children: [] }, { name: 'Brazil' }]
+ },
+ {
+ name: 'Europe',
+ children: [
+ { name: 'England' },
+ { name: 'Germany' },
+ { name: 'France' },
+ { name: 'Italy' },
+ { name: 'Spain' }
+ ]
+ }
+];
+
+const tree = new TreeModel();
+
+describe('treeTest', () => {
+ beforeAll(() => {
+ tree.setData({
+ nodes: _nodes,
+ options: null,
+ events: null
+ });
+ });
+
+ it('should return node branch requireds', () => {
+ // console.log('root', tree.getFirstRoot().data.name);
+ // console.log(tree.getNodeBy((node) => node.data.name === 'California').data.uuid);
+ // console.log(tree.getNodeBy((node) => node.data.name === 'California').id);
+ // console.log(tree.getNodeById(1));
+ const selectedNode = tree.getNodeBy(
+ node => node.data.name === 'California'
+ );
+ const result = getBranchRequierds(selectedNode, []);
+ const expected = [['New York', 'Florida'], ['United States']];
+
+ expect(result.length).toBeGreaterThan(1);
+ expect(result).toEqual(expected);
+ });
+
+ it('should return empty array - success state', () => {
+ const userSelect = ['Florida', 'New York', 'United States'];
+ const selectedNode = tree.getNodeBy(node => node.data.name === 'New York');
+ const result = validation(selectedNode, userSelect);
+
+ expect(result.length).toEqual(0);
+ expect(result).toEqual([]);
+ });
+
+ it('should return validation array - missing required filed', () => {
+ const userSelect = ['New York'];
+ const selectedNode = tree.getNodeBy(node => node.data.name === 'New York');
+ const result = validation(selectedNode, userSelect);
+ const expected = ['Florida', 'United States'];
+
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/public/src/app/rule-engine/version-type-select/version-type-select.component.html b/public/src/app/rule-engine/version-type-select/version-type-select.component.html
new file mode 100644
index 0000000..79b9eae
--- /dev/null
+++ b/public/src/app/rule-engine/version-type-select/version-type-select.component.html
@@ -0,0 +1,34 @@
+<div class="selected-event">
+
+ <div style="flex:1; display: flex; align-items: center;">
+
+ <span class="field-label required" style="margin-right: 10px;">Mapping Target</span>
+ <select name="mappingTarget" [(ngModel)]="mappingTarget" (ngModelChange)="onChangeMapping($event)" data-tests-id="mappingDdl"
+ style="height: 27px; padding: 0.3rem; margin-right: 18px;" class="field-select">
+ <option [ngValue]="null" disabled>Select Mapping</option>
+ <option *ngFor="let target of advancedSetting" [value]="target.name" data-tests-id="templateOptions">{{target.name}}</option>
+ </select>
+
+ <span class="field-label required" style="font-size: 13px; margin-right: 10px; display: flex;
+ align-items: center;" [ngClass]="{'required' : !readOnly}">
+ Version
+ </span>
+ <select *ngIf="!readOnly" style="height: 27px; padding: 0.3rem; margin-right: 18px;" [(ngModel)]="selectedVersion" (ngModelChange)="onSelectVersion($event)"
+ data-tests-id="selectVersion">
+ <option [ngValue]="null" disabled>Select Version</option>
+ <option *ngFor="let version of versions" [value]="version" data-tests-id="option">{{version}}</option>
+ </select>
+ <span *ngIf="readOnly" style="height: 27px; padding: 0.3rem; width:100px; margin-right: 18px; border: 1px solid #D2D2D2; display: flex; align-items: center; background: #F2F2F2">{{selectedVersion}}</span>
+
+ <span class="field-label required" style="font-size: 13px; display: flex; align-items: center; width: 100px;" [ngClass]="{'required' : !readOnly}">
+ Event Domain
+ </span>
+ <select *ngIf="!readOnly" style="height: 27px; padding: 0.3rem;" [(ngModel)]="selectedEvent" (ngModelChange)="onSelectEventType($event)"
+ data-tests-id="selectEventType">
+ <option [ngValue]="null" disabled>Select Type</option>
+ <option *ngFor="let event of events" [value]="event" data-tests-id="option">{{event | slice:0:event.length-6}}</option>
+ </select>
+ <span *ngIf="readOnly" style="height: 27px; padding: 0.3rem; width:200px; border: 1px solid #D2D2D2; display: flex; align-items: center; background: #F2F2F2">{{selectedEvent | slice:0:selectedEvent.length-6}}</span>
+ </div>
+
+</div>
diff --git a/public/src/app/rule-engine/version-type-select/version-type-select.component.scss b/public/src/app/rule-engine/version-type-select/version-type-select.component.scss
new file mode 100644
index 0000000..9f7bad3
--- /dev/null
+++ b/public/src/app/rule-engine/version-type-select/version-type-select.component.scss
@@ -0,0 +1,46 @@
+.selected-event {
+ display: flex;
+ margin: 10px 0;
+ // align-items: center;
+ flex-direction: column;
+ margin-bottom: 30px;
+}
+
+.small-padding {
+ padding-right: 1rem;
+}
+
+.btn {
+ padding: 6px;
+ margin: 6px 8px 6px 8px;
+ min-width: 88px;
+ border-radius: 3px;
+ font-size: 14px;
+ text-align: center;
+ text-transform: uppercase;
+ text-decoration: none;
+ border: none;
+ outline: none;
+}
+
+.target-field {
+ width: 370px;
+ display: flex;
+ align-items: center;
+ margin: 10px;
+ .field-label {
+ padding-right: 10px;
+ }
+ .required::before {
+ content: '*';
+ color: red;
+ padding-right: 5px;
+ }
+ .field-select {
+ flex: 1;
+ width: 100%;
+ min-width: 250px;
+ padding: 5px 0 5px 5px;
+ margin: 0;
+ }
+}
diff --git a/public/src/app/rule-engine/version-type-select/version-type-select.component.ts b/public/src/app/rule-engine/version-type-select/version-type-select.component.ts
new file mode 100644
index 0000000..b4170a5
--- /dev/null
+++ b/public/src/app/rule-engine/version-type-select/version-type-select.component.ts
@@ -0,0 +1,86 @@
+import { Component, Output, EventEmitter, Input } from '@angular/core';
+import { RuleEngineApiService } from '../api/rule-engine-api.service';
+import { Store } from '../../store/store';
+
+@Component({
+ selector: 'app-version-type-select',
+ templateUrl: './version-type-select.component.html',
+ styleUrls: ['./version-type-select.component.scss']
+})
+export class VersionTypeSelectComponent {
+ mappingTarget: string;
+ selectedEvent: String;
+ selectedVersion: String;
+ events: Array<String>;
+ loader: boolean;
+ editMode = false;
+ readOnly = false;
+ @Input() versions;
+ @Input() metaData;
+ @Output() nodesUpdated = new EventEmitter();
+ @Output() refrashRuleList = new EventEmitter();
+ advancedSetting;
+
+ constructor(private _ruleApi: RuleEngineApiService, public store: Store) {
+ this.selectedVersion = null;
+ this.selectedEvent = null;
+ // set ddl with the first option value.
+ this.mappingTarget = this.store.configurationForm[0].name;
+ this.advancedSetting = this.store.configurationForm.filter(item => {
+ if (
+ !(
+ item.hasOwnProperty('constraints') &&
+ !item.assignment.value.includes('get_input')
+ )
+ ) {
+ return item;
+ }
+ });
+ }
+
+ onChangeMapping(configurationKey) {
+ console.log('changing propertiy key:', configurationKey);
+ this._ruleApi.setFieldName(configurationKey);
+ this.refrashRuleList.next();
+ }
+
+ updateData(version, eventType, isList) {
+ this.selectedVersion = version;
+ this.selectedEvent = eventType;
+ this.readOnly = true;
+ }
+
+ updateVersionTypeFlag(flag) {
+ this.readOnly = flag;
+ if (flag === false) {
+ this.selectedVersion = null;
+ this.selectedEvent = null;
+ }
+ }
+
+ onSelectVersion(version, eventType) {
+ if (typeof eventType === 'undefined') {
+ this.selectedEvent = '';
+ this.events = this.metaData
+ .filter(x => x.version === version)
+ .map(x => x.eventTypes)[0];
+ if (eventType) {
+ this.editMode = true;
+ this.selectedEvent = eventType + 'Fields';
+ }
+ }
+ }
+
+ onSelectEventType(eventType) {
+ this.loader = true;
+ this._ruleApi
+ .getSchema(this.selectedVersion, this.selectedEvent)
+ .subscribe(tree => {
+ console.log('tree: ', tree);
+ this.loader = false;
+ this.nodesUpdated.emit({
+ nodes: tree
+ });
+ });
+ }
+}