diff options
Diffstat (limited to 'public/src/app/rule-engine')
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 + }); + }); + } +} |