diff options
Diffstat (limited to 'public/src/app')
75 files changed, 5048 insertions, 0 deletions
diff --git a/public/src/app/api/feather-pipe.ts b/public/src/app/api/feather-pipe.ts new file mode 100644 index 0000000..7a0715d --- /dev/null +++ b/public/src/app/api/feather-pipe.ts @@ -0,0 +1,19 @@ +import { DomSanitizer } from '@angular/platform-browser'; +import { Pipe, PipeTransform } from '@angular/core'; + +import * as feather from 'feather-icons/dist/feather'; + +@Pipe({ name: 'feather' }) +export class FeatherIconsPipe implements PipeTransform { + constructor(private sanitizer: DomSanitizer) {} + + transform(icon: string, size: number = 24, fill: string = 'none') { + return this.sanitizer.bypassSecurityTrustHtml( + feather.icons[icon].toSvg({ + width: size, + height: size, + fill: fill + }) + ); + } +} diff --git a/public/src/app/api/rest-api.service.spec.ts b/public/src/app/api/rest-api.service.spec.ts new file mode 100644 index 0000000..ce921cb --- /dev/null +++ b/public/src/app/api/rest-api.service.spec.ts @@ -0,0 +1,26 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { HttpModule } from '@angular/http'; +import { RestApiService } from './rest-api.service'; +import { v4 as genrateUuid } from 'uuid'; + +describe('RestApiService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpModule], + providers: [RestApiService] + }); + }); + + it( + 'should be created', + inject([RestApiService], (service: RestApiService) => { + expect(service).toBeTruthy(); + }) + ); + + it('should genrate deffrent uuid each time for request id', () => { + const firstUuid = genrateUuid(); + const secondUuid = genrateUuid(); + expect(firstUuid !== secondUuid).toBe(true); + }); +}); diff --git a/public/src/app/api/rest-api.service.ts b/public/src/app/api/rest-api.service.ts new file mode 100644 index 0000000..ba5cc54 --- /dev/null +++ b/public/src/app/api/rest-api.service.ts @@ -0,0 +1,179 @@ +import { Injectable } from '@angular/core'; +import { + Http, + Response, + Headers, + RequestOptions, + URLSearchParams +} from '@angular/http'; +import { Observable } from 'rxjs/Observable'; +// Import RxJs required methods +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/observable/throw'; +import { environment } from '../../environments/environment'; +import { v4 as uuidGenarator } from 'uuid'; + +@Injectable() +export class RestApiService { + options: RequestOptions; + headers: Headers; + baseUrl: string; + + constructor(private http: Http) { + this.baseUrl = `${environment.apiBaseUrl}`; + this.headers = new Headers({ + 'Content-Type': 'application/json', + USER_ID: 'ym903w' + }); + this.options = new RequestOptions({ headers: this.headers }); + } + + getVfcmtsForMigration(params) { + const { contextType, uuid, version } = params; + const url = `${ + this.baseUrl + }/${contextType}/${uuid}/${version}/getVfcmtsForMigration`; + this.options.headers.set('X-ECOMP-RequestID', uuidGenarator()); + return this.http + .get(url, this.options) + .map((res: Response) => res.json()) + .catch((error: any) => { + return Observable.throw(error.json() || 'Server error'); + }); + } + + getVfcmtReferenceData(vfcmtUUID) { + const url = `${this.baseUrl}/getVfcmtReferenceData/${vfcmtUUID}`; + this.options.headers.set('X-ECOMP-RequestID', uuidGenarator()); + return this.http + .get(url, this.options) + .map((res: Response) => res.json()) + .catch((error: any) => Observable.throw(error.json() || 'Server error')); + } + + getFlowType() { + const url = `${this.baseUrl}/conf/composition`; + this.options.headers.set('X-ECOMP-RequestID', uuidGenarator()); + return this.http + .get(url, this.options) + .map((res: Response) => res.json()) + .catch((error: any) => Observable.throw(error.json() || 'Server error')); + } + + createNewVFCMT(params) { + const url = `${this.baseUrl}/createMC`; + this.options.headers.set('X-ECOMP-RequestID', uuidGenarator()); + return this.http + .post(url, params, this.options) + .map((res: Response) => res.json()) + .catch((error: any) => { + return Observable.throw(error.json() || 'Server error'); + }); + } + + importVFCMT(params) { + const url = `${this.baseUrl}/importMC`; + this.options.headers.set('X-ECOMP-RequestID', uuidGenarator()); + return this.http + .post(url, params, this.options) + .map((res: Response) => res.json()) + .catch((error: any) => { + return Observable.throw(error.json() || 'Server error'); + }); + } + + getServiceInstances(serviceID) { + const url = `${this.baseUrl}/service/${serviceID}`; + this.options.headers.set('X-ECOMP-RequestID', uuidGenarator()); + return this.http + .get(url, this.options) + .map((res: Response) => res.json()) + .catch((error: any) => { + return Observable.throw(error.json() || 'Server error'); + }); + } + + getTemplateResources() { + const url = `${this.baseUrl}/getResourcesByMonitoringTemplateCategory`; + this.options.headers.set('X-ECOMP-RequestID', uuidGenarator()); + return this.http + .get(url, this.options) + .map((res: Response) => res.json()) + .catch((error: any) => Observable.throw(error.json() || 'Server error')); + } + + getMonitoringComponents(params) { + const { contextType, uuid, version } = params; + const url = `${ + this.baseUrl + }/${contextType}/${uuid}/${version}/monitoringComponents`; + this.options.headers.set('X-ECOMP-RequestID', uuidGenarator()); + return this.http + .get(url, this.options) + .map((res: Response) => res.json()) + .catch((error: any) => Observable.throw(error.json() || 'Server error')); + } + + deleteMonitoringComponent(params, vfcmtUuid, vfiName) { + const { contextType, uuid } = params; + const url = `${ + this.baseUrl + }/${contextType}/${uuid}/${vfiName}/${vfcmtUuid}/deleteVfcmtReference`; + this.options.headers.set('X-ECOMP-RequestID', uuidGenarator()); + return this.http + .delete(url, this.options) + .map((res: Response) => res.json()) + .catch((error: any) => Observable.throw(error.json() || 'Server error')); + } + + deleteMonitoringComponentWithBlueprint( + params, + monitoringComponentName, + vfcmtUuid, + vfiName + ) { + const { contextType, uuid } = params; + const url = `${ + this.baseUrl + }/${contextType}/${monitoringComponentName}/${uuid}/${vfiName}/${vfcmtUuid}/deleteVfcmtReference`; + this.options.headers.set('X-ECOMP-RequestID', uuidGenarator()); + return this.http + .delete(url, this.options) + .map((res: Response) => res.json()) + .catch((error: any) => Observable.throw(error.json() || 'Server error')); + } + + getCompositionMonitoringComponent(vfcmtUuid) { + const url = `${this.baseUrl}/getMC/${vfcmtUuid}`; + this.options.headers.set('X-ECOMP-RequestID', uuidGenarator()); + return this.http + .get(url, this.options) + .map((res: Response) => res.json()) + .catch((error: any) => Observable.throw(error.json() || 'Server error')); + } + + saveMonitoringComponent(params) { + const { contextType, serviceUuid, vfiName, vfcmtUuid, cdump } = params; + const url = `${ + this.baseUrl + }/${contextType}/${serviceUuid}/${vfiName}/saveComposition/${vfcmtUuid}`; + this.options.headers.set('X-ECOMP-RequestID', uuidGenarator()); + return this.http + .post(url, cdump, this.options) + .map((res: Response) => res.json()) + .catch((error: any) => Observable.throw(error.json() || 'Server error')); + } + + submitMonitoringComponent(params) { + const { contextType, serviceUuid, vfiName, vfcmtUuid, flowType } = params; + const url = `${ + this.baseUrl + }/${contextType}/createBluePrint/${vfcmtUuid}/${serviceUuid}/${vfiName}`; + this.options.headers.set('X-ECOMP-RequestID', uuidGenarator()); + return this.http + .post(url, {}, this.options) + .map((res: Response) => res.json()) + .catch((error: any) => Observable.throw(error.json() || 'Server error')); + } +} diff --git a/public/src/app/app-routing.module.ts b/public/src/app/app-routing.module.ts new file mode 100644 index 0000000..b2d1531 --- /dev/null +++ b/public/src/app/app-routing.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { HomeComponent } from './home/home.component'; +import { MainComponent } from './main/main.component'; + +const routes: Routes = [ + { + path: '', + redirectTo: '/home', + pathMatch: 'full' + }, + { + path: 'home', + component: HomeComponent + }, + { + path: 'main/:contextType/:uuid/:version/:mcid', + component: MainComponent + } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes, { useHash: true })], + exports: [RouterModule] +}) +export class AppRoutingModule {} diff --git a/public/src/app/app.component.html b/public/src/app/app.component.html new file mode 100644 index 0000000..adb06f1 --- /dev/null +++ b/public/src/app/app.component.html @@ -0,0 +1,7 @@ +<!-- <div class="container"> --> +<main [@slideAnimation]="getRouterOutletState(o)"> + <app-error-dialog></app-error-dialog> + <app-loader [hidden]="!this.store.loader"></app-loader> + <router-outlet #o="outlet"></router-outlet> +</main> +<!-- </div> --> diff --git a/public/src/app/app.component.scss b/public/src/app/app.component.scss new file mode 100644 index 0000000..82b9721 --- /dev/null +++ b/public/src/app/app.component.scss @@ -0,0 +1,20 @@ +:host { + display: flex; + overflow: auto; + height: 100vh; + + .container { + height: 100%; + } + + main { + flex: 1; + position: relative; + } + + /deep/ router-outlet ~ * { + position: absolute; + width: 100%; + // height: 100%; + } +} diff --git a/public/src/app/app.component.ts b/public/src/app/app.component.ts new file mode 100644 index 0000000..0711538 --- /dev/null +++ b/public/src/app/app.component.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; +import { slideAnimation } from './router.animations'; +import { ActivatedRoute } from '@angular/router'; +import { DomSanitizer } from '@angular/platform-browser'; +import { MatIconRegistry } from '@angular/material'; +import { Store } from './store/store'; + +@Component({ + selector: 'app-root', + animations: [slideAnimation], + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent { + constructor( + private _iconRegistry: MatIconRegistry, + private _sanitizer: DomSanitizer, + private route: ActivatedRoute, + public store: Store + ) { + this.loadIcons(_iconRegistry, _sanitizer); + } + + loadIcons(_iconRegistry: MatIconRegistry, _sanitizer: DomSanitizer) { + _iconRegistry.registerFontClassAlias('fontawesome', 'fa'); + } + + public getRouterOutletState(outlet) { + return outlet.isActivated ? outlet.activatedRoute : ''; + } +} diff --git a/public/src/app/app.module.ts b/public/src/app/app.module.ts new file mode 100644 index 0000000..8ed8c87 --- /dev/null +++ b/public/src/app/app.module.ts @@ -0,0 +1,109 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule, APP_INITIALIZER } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { HttpModule } from '@angular/http'; +import { HttpClientModule } from '@angular/common/http'; + +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MobxAngularModule } from 'mobx-angular'; + +import { TabViewModule, DialogModule, TooltipModule } from 'primeng/primeng'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatDialogModule } from '@angular/material/dialog'; +import { ToastrModule } from 'ngx-toastr'; +import { NgSelectModule } from '@ng-select/ng-select'; + +// import { SdcUiComponentsModule } from 'sdc-ui/lib/angular'; + +import { AppComponent } from './app.component'; +import { AppRoutingModule } from './app-routing.module'; +import { HomeComponent } from './home/home.component'; +import { GeneralComponent } from './general/general.component'; +import { MainComponent } from './main/main.component'; +import { RuleFrameComponent } from './rule-frame/rule-frame.component'; + +import { HostService } from './host/host.service'; +import { RestApiService } from './api/rest-api.service'; +import { FeatherIconsPipe } from './api/feather-pipe'; +import { Store } from './store/store'; +import { LoaderComponent } from './loader/loader.component'; +import { ErrorDialogComponent } from './error-dialog/error-dialog.component'; + +// rule engine +import { TreeModule } from 'angular-tree-component'; +import { TargetComponent } from './rule-engine/target/target.component'; +import { VersionTypeSelectComponent } from './rule-engine/version-type-select/version-type-select.component'; +import { FromComponent } from './rule-engine/from/from.component'; +import { ActionComponent } from './rule-engine/action/action.component'; +import { ActionListComponent } from './rule-engine/action-list/action-list.component'; +import { ConditionComponent } from './rule-engine/condition/condition.component'; +import { RuleEngineApiService } from './rule-engine/api/rule-engine-api.service'; +import { ConfirmPopupComponent } from './rule-engine/confirm-popup/confirm-popup.component'; +import { SlidePanelComponent } from './rule-engine/slide-panel/slide-panel.component'; +import { RuleListComponent } from './rule-engine/rule-list/rule-list.component'; +import { BarIconsComponent } from './bar-icons/bar-icons.component'; +import { DiagramComponent } from './diagram/diagram.component'; + +const appInitializerFn = () => { + return () => { + console.log('app initializing'); + }; +}; + +@NgModule({ + declarations: [ + AppComponent, + HomeComponent, + GeneralComponent, + MainComponent, + RuleFrameComponent, + LoaderComponent, + FeatherIconsPipe, + ErrorDialogComponent, + TargetComponent, + VersionTypeSelectComponent, + FromComponent, + ActionComponent, + ActionListComponent, + ConditionComponent, + ConfirmPopupComponent, + SlidePanelComponent, + RuleListComponent, + BarIconsComponent, + DiagramComponent + ], + imports: [ + BrowserModule, + BrowserAnimationsModule, + FormsModule, + HttpModule, + HttpClientModule, + AppRoutingModule, + MobxAngularModule, + TabViewModule, + DialogModule, + MatButtonModule, + MatIconModule, + MatDialogModule, + TreeModule, + NgSelectModule, + TooltipModule, + ToastrModule.forRoot({ enableHtml: true }) + ], + entryComponents: [ConfirmPopupComponent], + providers: [ + HostService, + RestApiService, + RuleEngineApiService, + Store, + { + provide: APP_INITIALIZER, + useFactory: appInitializerFn, + multi: true, + deps: [] + } + ], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/public/src/app/bar-icons/bar-icons.component.html b/public/src/app/bar-icons/bar-icons.component.html new file mode 100644 index 0000000..03129bf --- /dev/null +++ b/public/src/app/bar-icons/bar-icons.component.html @@ -0,0 +1,59 @@ +<div style="display: flex; position: relative; justify-content: flex-end;"> + <div style="display: flex; justify-content: flex-end;" [class]="genrateBarTestId()"> + <button mat-icon-button> + <span style="width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center;" [innerHTML]="'help-circle' | feather:18"></span> + </button> + <hr> + + <div *ngIf="tabName.includes('map')" style="display: flex;"> + <button mat-icon-button> + <span style="width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center;" [innerHTML]="'upload' | feather:18"></span> + </button> + <hr> + + <button mat-icon-button> + <span style="width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center;" [innerHTML]="'download' | feather:18"></span> + </button> + <hr> + </div> + + <button mat-icon-button (click)="enableSetting()" data-tests-id="setting-gear" [style.color]="this.store.expandAdvancedSetting[store.tabIndex] ? '#009FDB' : 'black'"> + <span style="width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center;" [innerHTML]="'settings' | feather:18"></span> + </button> + </div> + + <!-- advanced setting --> + <div class="setting" *ngIf="store.expandAdvancedSetting[store.tabIndex]"> + <div *mobxAutorun style="width: 100%;" [class]="tabName+'-setting-list'"> + <div style="font-size: 1.5em; padding: 0 12px;">{{tabName}} Advanced Setting</div> + <form #cdumpConfForm="ngForm"> + <div *ngFor="let prop of store.configurationForm" class="field" [id]="prop.name"> + <p class="field-label">{{prop.name}}</p> + <input *ngIf="!isPropertyDdl(prop)" type="text" name="{{prop.name}}" class="field-text" [(ngModel)]="prop.assignment.value" + (ngModelChange)="onChange($event)"> + <select *ngIf="isPropertyDdl(prop)" class="field-text" name="{{prop.name}}" [(ngModel)]="prop.assignment.value" (ngModelChange)="onChange($event)"> + <option *ngFor="let value of prop.constraints[0].valid_values" [value]="value"> + {{value}} + </option> + </select> + </div> + </form> + </div> + </div> +</div> diff --git a/public/src/app/bar-icons/bar-icons.component.scss b/public/src/app/bar-icons/bar-icons.component.scss new file mode 100644 index 0000000..893f757 --- /dev/null +++ b/public/src/app/bar-icons/bar-icons.component.scss @@ -0,0 +1,48 @@ +.setting { + position: absolute; + top: 47px; + right: 0; + background: white; + padding: 1em; + border: 1px solid gray; + display: flex; + min-width: 400px; + z-index: 2; + // width: 35%; +} + +.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; + } +} + +.field { + margin: 1em; + .field-label { + padding-bottom: 0.5em; + } + .field-text { + flex: 1; + width: 100%; + min-width: 250px; + padding: 5px 0 5px 5px; + margin: 0; + } +} diff --git a/public/src/app/bar-icons/bar-icons.component.ts b/public/src/app/bar-icons/bar-icons.component.ts new file mode 100644 index 0000000..adf4b88 --- /dev/null +++ b/public/src/app/bar-icons/bar-icons.component.ts @@ -0,0 +1,47 @@ +import { Component, Input, ViewChild } from '@angular/core'; +import { Store } from '../store/store'; +import { includes } from 'lodash'; +import { NgForm } from '@angular/forms'; + +@Component({ + selector: 'app-bar-icons', + templateUrl: './bar-icons.component.html', + styleUrls: ['./bar-icons.component.scss'] +}) +export class BarIconsComponent { + configuration; + @Input() tabName: string; + @ViewChild('cdumpConfForm') cdumpConfForm: NgForm; + + constructor(public store: Store) {} + + onChange(e) { + this.store.cdumpIsDirty = true; + } + + isPropertyDdl(property) { + if (property.hasOwnProperty('constraints')) { + if ( + includes( + property.constraints[0].valid_values, + property.assignment.value + ) + ) { + return true; + } else { + return false; + } + } else { + return false; + } + } + + genrateBarTestId() { + return `${this.tabName}-bar-icon-container`; + } + + enableSetting() { + this.store.expandAdvancedSetting[this.store.tabIndex] = !this.store + .expandAdvancedSetting[this.store.tabIndex]; + } +} diff --git a/public/src/app/diagram/diagram.component.html b/public/src/app/diagram/diagram.component.html new file mode 100644 index 0000000..b3cb28a --- /dev/null +++ b/public/src/app/diagram/diagram.component.html @@ -0,0 +1,19 @@ +<svg id="diagram" #diagram> + <svg viewBox="0 0 500 500" width="100%" height="500px" preserveAspectRatio="xMaxYMin meet" *ngFor="let item of list; let i = index"> + + <svg width="80px"> + <text x="0" [attr.y]="45 * (i+1)"> + {{item.source}} + </text> + </svg> + + <circle cx="100" [attr.cy]="44 * (i+1)" r="5" /> + <line x1="100" [attr.y1]="44 * (i+1)" [attr.x2]="maxWidth - 150" [attr.y2]="44 * (i+1)" stroke-width="2" stroke="black" stroke-dasharray="5, 5" + class="line" /> + <circle [attr.cx]="maxWidth - 150" [attr.cy]="44 * (i+1)" r="5" /> + + <text [attr.x]="maxWidth - 130" [attr.y]="45 * (i+1)"> + {{item.target}} + </text> + </svg> +</svg> diff --git a/public/src/app/diagram/diagram.component.scss b/public/src/app/diagram/diagram.component.scss new file mode 100644 index 0000000..57437d8 --- /dev/null +++ b/public/src/app/diagram/diagram.component.scss @@ -0,0 +1,28 @@ +svg { + height: 400px; + width: 100%; + margin: auto; + display: block; + .line { + stroke-dasharray: 1400; + animation: draw 5s ease-in; + } +} + +@keyframes draw { + from { + stroke-dashoffset: -1400; + } + to { + stroke-dashoffset: 0; + } +} + +@keyframes dude { + 0% { + width: 0; + } + 100% { + width: 100%; + } +} diff --git a/public/src/app/diagram/diagram.component.spec.ts b/public/src/app/diagram/diagram.component.spec.ts new file mode 100644 index 0000000..535f280 --- /dev/null +++ b/public/src/app/diagram/diagram.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DiagramComponent } from './diagram.component'; + +describe('DiagramComponent', () => { + let component: DiagramComponent; + let fixture: ComponentFixture<DiagramComponent>; + + beforeEach( + async(() => { + TestBed.configureTestingModule({ + declarations: [DiagramComponent] + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(DiagramComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/public/src/app/diagram/diagram.component.ts b/public/src/app/diagram/diagram.component.ts new file mode 100644 index 0000000..a0ae3a1 --- /dev/null +++ b/public/src/app/diagram/diagram.component.ts @@ -0,0 +1,12 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-diagram', + templateUrl: './diagram.component.html', + styleUrls: ['./diagram.component.scss'] +}) +export class DiagramComponent { + @Input() list; + maxWidth: number = 500; + constructor() {} +} diff --git a/public/src/app/error-dialog/error-dialog.component.html b/public/src/app/error-dialog/error-dialog.component.html new file mode 100644 index 0000000..7b72d06 --- /dev/null +++ b/public/src/app/error-dialog/error-dialog.component.html @@ -0,0 +1,17 @@ +<p-dialog [(visible)]="store.displayErrorDialog" modal="modal" width="500" [responsive]="true" data-tests-id="error-dialog"> + <p-header> + <span style="font-size: 1.3em;"> + Error + </span> + </p-header> + + <div *ngFor="let error of store.ErrorContent"> + {{ error.formattedErrorMessage }} + </div> + + <p-footer> + <button mat-raised-button color="primary" (click)="closeDialog()" data-tests-id="error-cancel"> + Cancel + </button> + </p-footer> +</p-dialog> diff --git a/public/src/app/error-dialog/error-dialog.component.scss b/public/src/app/error-dialog/error-dialog.component.scss new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/public/src/app/error-dialog/error-dialog.component.scss diff --git a/public/src/app/error-dialog/error-dialog.component.ts b/public/src/app/error-dialog/error-dialog.component.ts new file mode 100644 index 0000000..3e7bfe0 --- /dev/null +++ b/public/src/app/error-dialog/error-dialog.component.ts @@ -0,0 +1,17 @@ +import { Component, OnInit } from '@angular/core'; +import { Store } from '../store/store'; + +@Component({ + selector: 'app-error-dialog', + templateUrl: './error-dialog.component.html', + styleUrls: ['./error-dialog.component.scss'] +}) +export class ErrorDialogComponent implements OnInit { + constructor(public store: Store) {} + + ngOnInit() {} + + closeDialog() { + this.store.displayErrorDialog = false; + } +} diff --git a/public/src/app/general/general.component.html b/public/src/app/general/general.component.html new file mode 100644 index 0000000..dcea57a --- /dev/null +++ b/public/src/app/general/general.component.html @@ -0,0 +1,83 @@ +<form #generalForm="ngForm" novalidate style="display: flex; margin: 1em;"> + <div class="left"> + + <div class="import-wrapper" style="display: flex" *ngIf="store.generalflow === 'import' && !importCompleted"> + <div class="field" style="width:70%"> + <div class="field-label required" style="display: flex;"> + <span>Select existing VFCMT</span> + </div> + <ng-select name="vfcmt" [items]="vfcmts" required [virtualScroll]="true" placeholder="Select VFCMT" [(ngModel)]="selectedVfcmt" + class="vfcmt-list" (change)="vfcmtChange($event)"> + </ng-select> + </div> + + <div class="field" style="width:30%"> + <div class="field-label required" style="display: flex;"> + <span>Select version</span> + </div> + <select name="version" required data-tests-id="vfcmtVersion" [(ngModel)]="selectedVersion" [style.background]="versions.length == 0 ? '#ebebe4' : 'white'" + (ngModelChange)="versionChange($event)" [disabled]="versions.length == 0" style="width: 100%; height: 30px;"> + <option [ngValue]="null" disabled>Select version</option> + <option *ngFor="let item of versions" [value]="item.version">{{item.version}}</option> + </select> + </div> + </div> + + <div class="field"> + <div class="field-label required">Name</div> + <input type="text" name="name" ngModel required [(ngModel)]="newVfcmt.name" class="field-text" [disabled]="this.store.isEditMode || disableName" + data-tests-id="nameMc" /> + </div> + + <div class="field"> + <div class="field-label required">Description</div> + <textarea required name="description" ngModel [(ngModel)]="newVfcmt.description" style="resize: none;" cols="30" rows="10" + class="field-text" data-tests-id="descMc" [disabled]="this.store.isEditMode || disableDescription"></textarea> + </div> + + <div class="field" *ngIf="store.generalflow === 'new'"> + <div class="field-label required" style="display: flex;"> + <span>Template</span> + <span style="padding-left: 5px;" [innerHTML]="'help-circle' | feather:14"></span> + </div> + <select name="template" [disabled]="this.store.isEditMode" required [(ngModel)]="newVfcmt.template" (ngModelChange)="onChangeTemplate($event)" + data-tests-id="templateDdl" class="field-text" [style.background]="this.store.isEditMode ? '#ebebe4' : 'white'"> + <option [ngValue]="null" disabled>Select template</option> + <option *ngFor="let template of templates" [value]="template.uuid" data-tests-id="templateOptions">{{template.name}}</option> + </select> + </div> + + <div class="field" *ngIf="store.generalflow === 'import' || store.generalflow === 'edit'"> + <div class="field-label required" style="display: flex;"> + <span>Flow type</span> + <span style="padding-left: 5px;" [innerHTML]="'help-circle' | feather:14"></span> + </div> + <select name="flowType" [disabled]="this.store.isEditMode || disableFlowType" required [(ngModel)]="newVfcmt.flowType" data-tests-id="flowTypeDdl" + class="field-text" [style.background]="this.store.isEditMode || disableFlowType ? '#ebebe4' : 'white'"> + <option [ngValue]="null" disabled>Select Flow Type</option> + <option *ngFor="let flowType of flowTypes" [value]="flowType" data-tests-id="flowTypeOptions">{{flowType}}</option> + </select> + </div> + + <div class="field"> + <div class="field-label required" style="display: flex;"> + <span>Attached to</span> + <span style="padding-left: 5px;" [innerHTML]="'help-circle' | feather:14"></span> + </div> + <select name="serviceAttached" [disabled]="this.store.isEditMode || disableVnfiList" required [(ngModel)]="newVfcmt.vfni" + data-tests-id="vfniDdl" (ngModelChange)="onChangeVfni($event)" class="field-text" [style.background]="this.store.isEditMode || disableVnfiList ? '#ebebe4' : 'white'"> + <option [ngValue]="null" disabled>Select VFNI</option> + <option *ngFor="let vfi of vfniList" [value]="vfi.resourceInstanceName">{{vfi.resourceInstanceName}}</option> + </select> + </div> + </div> + + <div class="right"> + <div style="padding: 0.7em 0.5em; padding-top: 1em; font-weight: 600;">Flow diagram</div> + <div> + <app-diagram [list]="list"></app-diagram> + <!-- <img style="width:100%; height:100%;" src="https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Flag_of_Romania.svg/1200px-Flag_of_Romania.svg.png" + alt="flow"> --> + </div> + </div> +</form> diff --git a/public/src/app/general/general.component.scss b/public/src/app/general/general.component.scss new file mode 100644 index 0000000..d76e1ae --- /dev/null +++ b/public/src/app/general/general.component.scss @@ -0,0 +1,38 @@ +.left, +.right { + width: 50%; +} + +.ng-select.ng-single .ng-control { + border-radius: 0; + height: 30px; + min-height: 30px; +} + +.toast-container .toast { + width: 400px !important; + box-shadow: none; + border-radius: 0; +} +.toast-container .toast:hover { + box-shadow: none; +} + +.field { + margin: 1em; + .field-label { + padding-bottom: 0.5em; + } + .required::before { + content: '*'; + color: red; + padding-right: 5px; + } + .field-text { + flex: 1; + width: 100%; + min-width: 250px; + padding: 5px 0 5px 5px; + margin: 0; + } +} diff --git a/public/src/app/general/general.component.spec.ts b/public/src/app/general/general.component.spec.ts new file mode 100644 index 0000000..fb761db --- /dev/null +++ b/public/src/app/general/general.component.spec.ts @@ -0,0 +1,55 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { GeneralComponent, groupingData } from './general.component'; +import { sortBy } from 'lodash'; + +const data = [ + { + name: 'avi', + version: '2.0' + }, + { + name: 'stone', + version: '0.9' + }, + { + name: 'avi', + version: '2.1' + }, + { + name: 'vosk', + version: '0.1' + }, + { + name: 'liav', + version: '0.5' + } +]; +const sortedMatchVfcmtList = ['avi', 'liav', 'stone', 'vosk']; +const sortedVersionInGroup = [ + { + name: 'avi', + version: '2.1' + }, + { + name: 'avi', + version: '2.0' + } +]; + +describe('GeneralComponent', () => { + it('should sort vfcmt by A to Z', () => { + const sorted = groupingData(data); + const vfcmtList = sortBy(Object.keys(sorted), name => name); + expect(vfcmtList).toEqual(sortedMatchVfcmtList); + }); + + it('should group vfcmt by name', () => { + const sorted = groupingData(data); + expect(Object.keys(sorted)).toEqual(['avi', 'stone', 'vosk', 'liav']); + }); + + it('should version array be sorted in group', () => { + const sorted = groupingData(data); + expect(Object.values(sorted)[0]).toEqual(sortedVersionInGroup); + }); +}); diff --git a/public/src/app/general/general.component.ts b/public/src/app/general/general.component.ts new file mode 100644 index 0000000..422d834 --- /dev/null +++ b/public/src/app/general/general.component.ts @@ -0,0 +1,323 @@ +import { + Component, + OnInit, + ViewChild, + ViewEncapsulation, + Output, + EventEmitter +} from '@angular/core'; +import { RestApiService } from '../api/rest-api.service'; +import { ActivatedRoute } from '@angular/router'; +import { Store } from '../store/store'; +import { NgForm } from '@angular/forms'; +import { forkJoin } from 'rxjs/observable/forkJoin'; +import { + pipe, + groupBy, + map, + sort, + descend, + ascend, + prop, + find, + propEq, + findIndex +} from 'ramda'; +import { sortBy, forEach } from 'lodash'; +import { ToastrService } from 'ngx-toastr'; + +export const groupingData = pipe( + groupBy(prop('name')), + map(sort(descend(prop('version')))) +); + +@Component({ + selector: 'app-general', + encapsulation: ViewEncapsulation.None, + templateUrl: './general.component.html', + styleUrls: ['./general.component.scss'] +}) +export class GeneralComponent implements OnInit { + newVfcmt = { + name: null, + description: null, + template: null, + flowType: null, + vfni: null, + isCloneVFCMT: false, + isUpdateFlowType: false + }; + isLatestVersion = true; + vfniList = []; + templates = []; + serviceUUID: string; + vfcmts = new Array(); + versions = new Array(); + result = new Array(); + flowTypes = new Array(); + selectedVfcmt; + selectedVersion = null; + importCompleted = false; + disableName = false; + disableDescription = false; + disableFlowType = false; + disableVnfiList = false; + @Output() updateCdumpEv = new EventEmitter<string>(); + @ViewChild('generalForm') generalForm; + // list = [ + // { source: 'node1dsvsdsvd', target: 'node2' }, + // { source: 'node3', target: 'node4' }, + // { source: 'node5', target: 'nodedsvsds6' }, + // { source: 'node7', target: 'node8' } + // ]; + list = []; + + constructor( + private restApi: RestApiService, + public store: Store, + private toastr: ToastrService, + private route: ActivatedRoute + ) { + console.log('route mcid: ', this.route.snapshot.params.mcid); + if ( + this.route.snapshot.params.mcid === 'import' || + this.route.snapshot.params.mcid === 'new' + ) { + this.store.generalflow = this.route.snapshot.params.mcid; + } else { + this.store.generalflow = 'edit'; + this.store.mcUuid = this.route.snapshot.params.mcid; + } + this.serviceUUID = this.route.snapshot.params.uuid; + } + + onChangeTemplate(template) { + console.log('flow template', template); + } + onChangeVfni(vfni) { + console.log('vfni', vfni); + } + vfcmtChange(vfcmtName) { + vfcmtName !== undefined + ? (this.versions = this.result[vfcmtName]) + : ((this.versions = []), this.restForm()); + this.store.isEditMode = true; + this.selectedVersion = null; + } + versionChange(version) { + const versionIndex = findIndex(propEq('version', version))(this.versions); + this.isLatestVersion = versionIndex === 0 ? true : false; + const selectedVfcmtByVersion = find( + propEq('version', version), + this.result[this.selectedVfcmt] + ); + this.newVfcmt.template = selectedVfcmtByVersion.uuid; + this.restApi.getVfcmtReferenceData(selectedVfcmtByVersion.uuid).subscribe( + success => { + this.store.loader = false; + console.log('vfcmt ref data:', success); + this.store.isEditMode = false; + this.getServiceRef(success); + }, + error => { + this.notifyError(error); + }, + () => { + this.store.loader = false; + } + ); + } + private notifyError(error: any) { + this.store.loader = false; + console.log(error.notes); + this.store.ErrorContent = Object.values(error.requestError); + this.store.displayErrorDialog = true; + } + + ngOnInit() { + if (this.store.generalflow === 'edit') { + this.store.loader = true; + this.restApi + .getCompositionMonitoringComponent(this.store.mcUuid) + .subscribe( + response => { + this.newVfcmt = response.vfcmt; + this.flowTypes.push(this.newVfcmt.flowType); + this.newVfcmt.vfni = this.store.vfiName; + this.vfniList.push({ resourceInstanceName: this.newVfcmt.vfni }); + // this.store.cdump = response.cdump; + this.updateCdumpEv.next(response.cdump); + this.store.isEditMode = true; + this.store.loader = false; + }, + error => { + this.notifyError(error); + } + ); + } else if (this.store.generalflow === 'import') { + this.store.loader = true; + this.store.isEditMode = true; + this.restApi + .getVfcmtsForMigration({ + contextType: this.route.snapshot.params.contextType, + uuid: this.route.snapshot.params.uuid, + version: this.route.snapshot.params.version + }) + .subscribe( + success => { + this.store.loader = false; + this.result = groupingData(success); + this.vfcmts = sortBy(Object.keys(this.result), name => name); + }, + error => { + this.notifyError(error); + }, + () => { + this.store.loader = false; + } + ); + } else if (this.route.snapshot.params.mcid === 'new') { + // get template data for ddl + const template$ = this.restApi.getTemplateResources(); + // get service vfi list for ddl '08ff91f1-9b57-4918-998b-4d2c98832815' + const vfniList$ = this.restApi.getServiceInstances(this.serviceUUID); + this.store.loader = true; + forkJoin(template$, vfniList$).subscribe( + success => { + console.log('all', success); + this.templates = success[0]; + this.vfniList = success[1].resources; + }, + error => { + this.notifyError(error); + }, + () => { + this.store.loader = false; + } + ); + } + } + + private restForm() { + this.newVfcmt = { + name: null, + description: null, + template: null, + flowType: null, + vfni: null, + isCloneVFCMT: false, + isUpdateFlowType: false + }; + const controls = this.generalForm.controls; + forEach(controls, control => { + control.markAsUntouched(); + }); + } + + private getServiceRef(data) { + if (data.flowType !== undefined) { + if (data.serviceUuid === this.serviceUUID) { + this.newVfcmt.name = data.name; + this.newVfcmt.description = data.description; + this.disableName = true; + this.disableDescription = true; + this.setFlowType(data); // true + this.setVfni(data); + this.newVfcmt.isCloneVFCMT = false; + } else { + this.isCloneVfcmtToast(); + this.setFlowType(data); // true + this.setVfni(data); + this.newVfcmt.isCloneVFCMT = true; + } + } else { + if (data.serviceUuid === this.serviceUUID && this.isLatestVersion) { + this.newVfcmt.name = data.name; + this.newVfcmt.description = data.description; + this.disableName = true; + this.disableDescription = true; + this.newVfcmt.isCloneVFCMT = false; + this.setFlowType(data); // true + this.setVfni(data); + } else { + this.isCloneVfcmtToast(); + this.setFlowType(data); // true + this.setVfni(data); + this.newVfcmt.isCloneVFCMT = true; + } + } + } + + private isCloneVfcmtToast() { + this.toastr.warning( + `<h3>The monitoring configuration is copied.</h3> + <div> + The selected VFCMT is assigned to a different + </div> + <div> + service or has a newer version. + </div> + `, + '', + { + enableHtml: true, + // disableTimeOut: true + timeOut: 10000 + } + ); + } + + private setVfni(data: any) { + if (data.serviceUuid !== this.serviceUUID) { + this.getVfniList(); + this.disableVnfiList = false; + } else { + this.disableVnfiList = true; + this.vfniList.push({ resourceInstanceName: data.vfiName }); + this.newVfcmt.vfni = data.vfiName; + } + } + + private setFlowType(data: any) { + if (data.flowType === undefined) { + this.newVfcmt.isUpdateFlowType = true; + this.disableFlowType = false; + this.getFlowTypeList(); + } else { + this.newVfcmt.isUpdateFlowType = false; + this.disableFlowType = true; + this.flowTypes.push(data.flowType); + this.newVfcmt.flowType = data.flowType; + } + } + + private getFlowTypeList() { + this.restApi.getFlowType().subscribe( + success => { + console.log('flow types', success.flowTypes); + this.flowTypes = success.flowTypes; + }, + error => { + this.notifyError(error); + }, + () => { + this.store.loader = false; + } + ); + } + private getVfniList() { + this.restApi.getServiceInstances(this.serviceUUID).subscribe( + success => { + console.log('vfni List', success); + this.vfniList = success.resources; + }, + error => { + this.notifyError(error); + return null; + }, + () => { + this.store.loader = false; + } + ); + } +} diff --git a/public/src/app/home/home.component.html b/public/src/app/home/home.component.html new file mode 100644 index 0000000..90e82d3 --- /dev/null +++ b/public/src/app/home/home.component.html @@ -0,0 +1,106 @@ +<div class="container"> + <div style="display: flex; + justify-content: space-between;"> + <div style="font-size: 1.7em; display: flex; align-items: center;">Monitoring</div> + + <div style="display: flex;"> + <button mat-icon-button [disabled]="checkCanCreate()" (click)="importScreen()"> + <span style="width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center;" [innerHTML]="'download' | feather:22"></span> + </button> + + <button mat-raised-button color="primary" (click)="createScreen()" data-tests-id="btn-create-mc" [disabled]="checkCanCreate()"> + Create New MC + </button> + </div> + </div> + + <div *ngIf="showTable===true; then thenBlock else elseBlock"></div> + + <ng-template #thenBlock> + <!-- Table --> + <div class="table-wrapper"> + <div *ngIf="unavailableMonitoringComponents.length > 0" data-tests-id="unavailableArea" style="color: white; background: red; padding: 1rem; border-radius: 5px; font-weight: bold; margin: 1em 0;"> + <div *ngFor="let item of unavailableMonitoringComponents"> + {{item.uuid}} + </div> + </div> + <table class="mcTable"> + <thead> + <tr data-tests-id="monitoringComponentTableHeaders"> + <th>Monitoring Configuration</th> + <th>VNFI Name</th> + <th style="width:90px;">Version</th> + <th style="width:140px;">Status</th> + <th style="width:140px;">Last Updated by</th> + <th style="width:96px;">Action</th> + </tr> + </thead> + <tbody> + <tr *ngFor="let item of monitoringComponents; let i = index" on-mouseleave="hoveredIndex=null" (click)="onSelect(i)" [class.active]="i == selectedLine" + data-tests-id="monitoringComponentTableItems" on-mouseover="hoveredIndex=i"> + <td color="blue"> + <div [hidden]="checkHoverCondition(item)" data-tests-id="tableItemsMonitoringConfiguration" class="table-Monitoring-Component" (click)="editItem(item)"> + {{item.name}} + </div> + </td> + <td> + <span pTooltip="{{item.vfiName}}" tooltipPosition="bottom" style="padding:5px;">{{item.vfiName}}</span> + </td> + <td style="width:90px;">{{item.version}}</td> + <td style="width:140px;">{{item.status}}</td> + <td style="width:140px;">{{item.lastUpdaterUserId}}</td> + <td style="width:80px;"> + <div *ngIf="i==hoveredIndex" [hidden]="checkHoverCondition(item)"> + <button mat-icon-button data-tests-id="tableItemsButtonDelete" (click)="deleteItem(item)" style="width:30px; height: 30px;"> + <span style="width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center;" [innerHTML]="'trash-2' | feather:18"></span> + </button> + </div> + <div *ngIf="i==hoveredIndex" [hidden]="!checkHoverCondition(item)"> + <button mat-icon-button data-tests-id="tableItemsButtonInfo" style="width:30px; height: 30px;"> + <span style="width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center;" [innerHTML]="'info' | feather:18"></span> + </button> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </ng-template> + + <ng-template #elseBlock> + <div style="display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + flex:1;"> + <div style="font-size: 1.5em;"> + Monitoring Configuration does not Exist + </div> + <div style="padding: 0.5em; padding-top: 1em;" data-tests-id="new-monitoring-title"> + A Monitoring Configuration (MC) was not yet created + </div> + <div> + Please create a new MC to monitor the service + </div> + <div class="wrapper-btn-add-mc"> + <button mat-mini-fab color="primary" (click)="createScreen()" data-tests-id="btn-fab-create-mc" [disabled]="checkCanCreate()"> + <span [innerHTML]="'plus' | feather:24"></span> + </button> + <span data-tests-id="btn-span-create-mc" style="margin-top: 1rem; font-size: 1.2em; font-weight: 400;" [style.color]="checkCanCreate() ? '#ebebe4' : '#009FDB'">Add First MC</span> + </div> + </div> + </ng-template> +</div> + diff --git a/public/src/app/home/home.component.scss b/public/src/app/home/home.component.scss new file mode 100644 index 0000000..583705f --- /dev/null +++ b/public/src/app/home/home.component.scss @@ -0,0 +1,110 @@ +.container { + display: flex; + flex-direction: column; + height: 100%; + padding: 0.5em; + margin-left: 15px; + margin-right: 15px; + .wrapper-btn-add-mc { + margin-top: 3em; + display: flex; + flex-direction: column; + align-items: center; + } +} + +.table-Monitoring-Component { + &:hover { + color: #009fdb; + text-decoration: underline; + cursor: pointer; + } +} + +.table-wrapper { + display: flex; + justify-content: center; + flex: 1; + margin-bottom: 2em; + flex-direction: column; + display: block; +} + +table.mcTable { + display: flex; + flex-flow: column; + height: calc(100vh - 150px); + width: 100%; + background-color: #ffffff; + color: #5a5a5a; +} +table.mcTable thead { + /* head takes the height it requires, + and it's not scaled when table.mcTable is resized */ + flex: 0 0 auto; + // width: calc(100% - 17px); + width: 100%; +} +table.mcTable tbody { + /* body takes all the remaining available space */ + flex: 1 1 auto; + display: block; + overflow-y: scroll; +} +table.mcTable tbody tr { + width: 100%; +} + +table.mcTable thead, +table.mcTable tbody tr { + display: table; + table-layout: fixed; +} + +table.mcTable { + border-collapse: collapse; + border-spacing: 0px; +} + +table.mcTable tr.active td { + background-color: #e6f6fb !important; + color: #5a5a5a; +} + +table.mcTable th { + background: #f4f4f4; + color: #191919; + text-align: left; +} + +table.mcTable tr { + &:hover { + background-color: #f8f8f8; + color: #5a5a5a; + } +} + +table.mcTable table, +table.mcTable th, +table.mcTable td { + padding: 5px 10px; + border: 0.5px solid #d2d2d2; + border-bottom: none; + height: 40px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +table.mcTable tr:last-child { + border-bottom: 0.5px solid #d2d2d2; +} + +/deep/ .ui-tooltip .ui-tooltip-text { + font-size: 0.8em; + padding: 0.7em; +} + +/deep/ .ui-tooltip { + max-width: 400px; +} diff --git a/public/src/app/home/home.component.ts b/public/src/app/home/home.component.ts new file mode 100644 index 0000000..1c538c0 --- /dev/null +++ b/public/src/app/home/home.component.ts @@ -0,0 +1,188 @@ +import { Component } from '@angular/core'; +import { Store } from '../store/store'; +import { HostService } from '../host/host.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RestApiService } from '../api/rest-api.service'; +import { NgIf } from '@angular/common'; +import { ConfirmPopupComponent } from '../rule-engine/confirm-popup/confirm-popup.component'; +import { MatDialog } from '@angular/material'; +import { ToastrService } from 'ngx-toastr'; +import { ChangeDetectorRef } from '@angular/core'; + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'] +}) +export class HomeComponent { + linkToMain: string; + currentUserId: string; + showTable = true; + selectedLine; + monitoringComponents = new Array(); + unavailableMonitoringComponents = new Array(); + hoveredIndex = null; + dialogRef; + + constructor( + private activeRoute: ActivatedRoute, + private route: Router, + private _restApi: RestApiService, + private dialog: MatDialog, + public store: Store, + private toastr: ToastrService, + private changeDetectorRef: ChangeDetectorRef + ) { + this.store.loader = true; + this.activeRoute.queryParams.subscribe(params => { + console.log('params: %o', params); + this.store.sdcParmas = params; + this.linkToMain = `/main/${params.contextType}/${params.uuid}/${ + params.version + }/`; + this._restApi.getMonitoringComponents(params).subscribe( + response => { + console.log('response: ', response); + if (response.hasOwnProperty('monitoringComponents')) { + this.monitoringComponents = response.monitoringComponents; + } + if (response.hasOwnProperty('unavailable')) { + this.unavailableMonitoringComponents = response.unavailable; + } + this.showTable = this.monitoringComponents.length > 0; + this.store.loader = false; + }, + response => { + this.showTable = false; + this.store.loader = false; + console.log('ERROR: ', response); + } + ); + HostService.disableLoader(); + }); + } + + createScreen() { + this.store.isEditMode = false; + this.route.navigate([this.linkToMain + 'new']); + } + + importScreen() { + this.store.isEditMode = false; + this.route.navigate([this.linkToMain + 'import']); + } + + checkCanCreate() { + if ( + JSON.parse(this.store.sdcParmas.isOwner) && + this.store.sdcParmas.lifecycleState === 'NOT_CERTIFIED_CHECKOUT' + ) { + return false; + } else { + return true; + } + } + + checkHoverCondition(item: any): boolean { + if ( + this.store.sdcParmas.userId === item.lastUpdaterUserId && + this.store.sdcParmas.lifecycleState === 'NOT_CERTIFIED_CHECKOUT' + ) { + return false; + } else { + return true; + } + } + + editItem(item: any): void { + this.store.vfiName = item.vfiName; + this.route.navigate([this.linkToMain + '/' + item.uuid]); + } + + onSelect(item: any): void { + this.selectedLine = item; + console.log('selected : ', item); + } + + deleteEnable(item: any): boolean { + console.log( + 'delete enable: ', + item.isOwner && item.Lifecycle == 'NOT_CERTIFIED_CHECKOUT' + ); + const { userId, lifecycleState } = this.store.sdcParmas; + return ( + item.lastUpdaterUserId == userId && + lifecycleState == 'NOT_CERTIFIED_CHECKOUT' + ); + } + + deleteItem(item: any): void { + let deleteRow = this.hoveredIndex; + 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) { + if (item.status == 'submitted') { + this._restApi + .deleteMonitoringComponentWithBlueprint( + this.store.sdcParmas, + item.name, + item.uuid, + item.vfiName + ) + .subscribe( + response => { + this.itemDeletedRemoveAndNotify(deleteRow); + }, + error => { + if (error.messageId === 'SVC6118') { + this.monitoringComponents.splice(deleteRow, 1); + this.changeDetectorRef.detectChanges(); + } + const errorMsg = Object.values(error.requestError) as any; + this.toastr.error('', errorMsg[0]); + } + ); + } else { + this._restApi + .deleteMonitoringComponent( + this.store.sdcParmas, + item.uuid, + item.vfiName + ) + .subscribe( + response => { + this.itemDeletedRemoveAndNotify(deleteRow); + }, + error => { + const errorMsg = Object.values(error.requestError) as any; + this.toastr.error('', errorMsg[0]); + } + ); + } + } + }); + } + + itemDeletedRemoveAndNotify(deletedRow: number): void { + this.monitoringComponents.splice(deletedRow, 1); + this.changeDetectorRef.detectChanges(); + this.toastr.success( + '', + 'Monitoring Configuration was successfully deleted' + ); + } + + // convertFile(fileInput: any) { + // // read file from input + // const fileReaded = fileInput.target.files[0]; + // Papa.parse(fileReaded, { + // complete: function(results) { + // console.log('Finished:', results.data); + // } + // }); + // } +} diff --git a/public/src/app/host/host.service.ts b/public/src/app/host/host.service.ts new file mode 100644 index 0000000..31c4746 --- /dev/null +++ b/public/src/app/host/host.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core'; + +interface HostParams { + readonly userId: string; + readonly contextType: string; + readonly vfcmtUuid: string; + readonly lifecycleState: string; + readonly isOwner: string; +} + +@Injectable() +export class HostService { + /* Public Members */ + public static getParams(): HostParams { + return this.getQueryParamsObj(window.location.hash) as HostParams; + } + + public static disableLoader(): void { + this.postMessage('READY', null); + } + + /* 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(7) // 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/loader/loader.component.html b/public/src/app/loader/loader.component.html new file mode 100644 index 0000000..55a8f4c --- /dev/null +++ b/public/src/app/loader/loader.component.html @@ -0,0 +1,4 @@ +<!-- loader --> +<div class="tlv-loader-block"> +</div> +<div class="tlv-loader large" style="z-index: 10002;" data-tests-id="loader"></div> diff --git a/public/src/app/loader/loader.component.scss b/public/src/app/loader/loader.component.scss new file mode 100644 index 0000000..621adba --- /dev/null +++ b/public/src/app/loader/loader.component.scss @@ -0,0 +1,152 @@ +.tlv-loader-block { + background-color: #ffffff; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + opacity: 0.5; +} + +.tlv-loader { + height: 63px; + width: 63px; + position: absolute; + top: 50%; + left: 50%; +} + +.tlv-loader.small { + -webkit-transform: scale(0.26); + transform: scale(0.26); + margin-top: -36.5px; + margin-left: -36.5px; +} + +.tlv-loader.medium { + -webkit-transform: scale(0.5); + transform: scale(0.5); + margin-top: -26.5px; + margin-left: -26.5px; +} + +.tlv-loader.large { + -webkit-transform: scale(1); + transform: scale(1); + margin-top: -10.5px; + margin-left: -10.5px; +} + +.tlv-loader::before { + background-color: #eaeaea; + border-radius: 50%; + box-shadow: 21px 21px 0px 0px #eaeaea, 0px 42px 0px 0px #eaeaea, + -21px 21px 0px 0px #eaeaea; + content: ''; + display: block; + height: 21px; + width: 21px; + position: absolute; + left: 50%; + margin-left: -10.5px; +} + +.tlv-loader::after { + border-radius: 50%; + content: ''; + display: block; + position: absolute; + height: 21px; + width: 21px; + -webkit-animation: dot-move 4.5s infinite ease-in; + animation: dot-move 4.5s infinite ease-in; +} + +@keyframes dot-move { + 0% { + background-color: #3bb2df; + left: 21px; + top: 0; + } + 6.25% { + background-color: #3bb2df; + left: 42px; + top: 21px; + } + 12.5% { + background-color: #3bb2df; + left: 21px; + top: 42px; + } + 18.75% { + background-color: #3bb2df; + left: 0; + top: 21px; + } + 25% { + background-color: #ffb81c; + left: 21px; + top: 0; + } + 31.25% { + background-color: #ffb81c; + left: 42px; + top: 21px; + } + 37.5% { + background-color: #ffb81c; + left: 21px; + top: 42px; + } + 43.75% { + background-color: #ffb81c; + left: 0; + top: 21px; + } + 50% { + background-color: #caa2dd; + left: 21px; + top: 0; + } + 56.25% { + background-color: #caa2dd; + left: 42px; + top: 21px; + } + 62.5% { + background-color: #caa2dd; + left: 21px; + top: 42px; + } + 68.75% { + background-color: #caa2dd; + left: 0; + top: 21px; + } + 75% { + background-color: #d9e51c; + left: 21px; + top: 0; + } + 81.25% { + background-color: #d9e51c; + left: 42px; + top: 21px; + } + 87.5% { + background-color: #d9e51c; + left: 21px; + top: 42px; + } + 93.75% { + background-color: #d9e51c; + left: 0; + top: 21px; + } + 100% { + background-color: #3bb2df; + left: 21px; + top: 0; + } +} diff --git a/public/src/app/loader/loader.component.spec.ts b/public/src/app/loader/loader.component.spec.ts new file mode 100644 index 0000000..7c82913 --- /dev/null +++ b/public/src/app/loader/loader.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoaderComponent } from './loader.component'; + +describe('LoaderComponent', () => { + let component: LoaderComponent; + let fixture: ComponentFixture<LoaderComponent>; + + beforeEach( + async(() => { + TestBed.configureTestingModule({ + declarations: [LoaderComponent] + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(LoaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/public/src/app/loader/loader.component.ts b/public/src/app/loader/loader.component.ts new file mode 100644 index 0000000..89403b2 --- /dev/null +++ b/public/src/app/loader/loader.component.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-loader', + templateUrl: './loader.component.html', + styleUrls: ['./loader.component.scss'] +}) +export class LoaderComponent implements OnInit { + constructor() {} + + ngOnInit() {} +} diff --git a/public/src/app/main/main.component.html b/public/src/app/main/main.component.html new file mode 100644 index 0000000..d54b27b --- /dev/null +++ b/public/src/app/main/main.component.html @@ -0,0 +1,62 @@ +<div class="container"> + + <div style="padding: .25em; display: flex; + justify-content: space-between; + align-items: flex-end;"> + <div> + <a (click)="goBack()" data-tests-id="btn-back-home" style="display: flex; cursor: pointer;text-decoration: none; color: #009fdb;"> + <mat-icon fontSet="fontawesome" fontIcon="fa-angle-left" style="height: 17px; width: 12px; font-size: 17px;"></mat-icon> + <span style="display: flex; align-items: center;">Back to Monitoring</span> + </a> + <div style="margin:10px 0;" data-tests-id="new-monitorying-titie"> + <span style="font-size: 2em;" *ngIf='store.generalflow === "new"'> + New + </span> + <span style="font-size: 2em;" *ngIf='store.generalflow === "import"'> + Import + </span> + <span style="font-size: 2em;"> + Monitoring Configuration + </span> + </div> + </div> + + <div> + <div *ngIf='store.generalflow === "new" || store.generalflow === "edit"'> + <button *ngIf="!this.store.isEditMode" mat-raised-button color="primary" [disabled]="this.generalComponent.generalForm.invalid" + data-tests-id="createMonitoring" (click)="createMC(this.generalComponent.generalForm.value)">Create</button> + + <div *ngIf="this.store.isEditMode" style="display: flex;"> + <button mat-icon-button (click)="saveCDUMP()" [disabled]="!store.cdumpIsDirty"> + <span style="width: 100%; + height: 100%; + padding-right: 20px; + display: flex; + justify-content: center; + align-items: center;" [innerHTML]="'save' | feather:22"></span> + </button> + <button mat-raised-button color="primary" (click)="saveAndCreateBlueprint()">Submit</button> + </div> + </div> + <div *ngIf='store.generalflow === "import"'> + <button mat-raised-button color="primary" (click)="importMC(this.generalComponent.newVfcmt)" [disabled]="this.generalComponent.generalForm.invalid" + data-tests-id="importMonitoring">Import</button> + </div> + </div> + </div> + + <div style="position: relative; flex:1;"> + + <p-tabView (onChange)="handleChange($event)" data-tests-id="tabs"> + <p-tabPanel header="General"> + <div> + <app-general (updateCdumpEv)="updateCdump($event)"></app-general> + </div> + </p-tabPanel> + <p-tabPanel *ngFor="let item of nodes" [header]="item.name"> + <app-rule-frame [tabName]="item.name"></app-rule-frame> + </p-tabPanel> + </p-tabView> + + </div> +</div> diff --git a/public/src/app/main/main.component.scss b/public/src/app/main/main.component.scss new file mode 100644 index 0000000..402a56a --- /dev/null +++ b/public/src/app/main/main.component.scss @@ -0,0 +1,33 @@ +.container { + display: flex; + flex-direction: column; + // height: 100%; + margin: 1em; +} + +// overhide +.ui-tabview .ui-tabview-panel { + border: 1px solid #d9d9d9; + padding: 0; + margin-left: 0.2em; + height: calc(100vh - 150px); + overflow: auto; +} + +.ui-tabview .ui-tabview-nav li { + margin: 0; +} + +.ui-tabview .ui-tabview-nav li.ui-tabview-selected { + color: #009fdb; + border-top: 4px solid #009fdb; + border-bottom: none; +} + +.ui-tabview-title { + font-size: 14px; +} + +.ui-tabview .ui-tabview-nav li.ui-tabview-selected .ui-tabview-title { + color: #009fdb; +} diff --git a/public/src/app/main/main.component.ts b/public/src/app/main/main.component.ts new file mode 100644 index 0000000..fdbb077 --- /dev/null +++ b/public/src/app/main/main.component.ts @@ -0,0 +1,228 @@ +import { Component, ViewEncapsulation, ViewChild } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Location } from '@angular/common'; +import { RestApiService } from '../api/rest-api.service'; +import { Store } from '../store/store'; +import { RuleFrameComponent } from '../rule-frame/rule-frame.component'; +import { GeneralComponent } from '../general/general.component'; +import { ToastrService } from 'ngx-toastr'; +import { forkJoin } from 'rxjs/observable/forkJoin'; + +@Component({ + selector: 'app-main', + encapsulation: ViewEncapsulation.None, + templateUrl: './main.component.html', + styleUrls: ['./main.component.scss'] +}) +export class MainComponent { + cdump; + nodes = []; + @ViewChild(GeneralComponent) generalComponent: GeneralComponent; + // @ViewChildren(RuleFrameComponent) ruleFrameRef: QueryList<RuleFrameComponent>; + + constructor( + private route: ActivatedRoute, + private restApi: RestApiService, + private toastr: ToastrService, + public store: Store, + private location: Location + ) { + this.route.snapshot.params.mcid === 'import' + ? (this.store.generalflow = 'import') + : (this.store.generalflow = 'new'); + } + + goBack() { + this.location.back(); + } + + createMC(params) { + console.log('newVfcmt: %o', params); + this.store.loader = true; + this.restApi + .createNewVFCMT({ + name: params.name, + description: params.description, + templateUuid: params.template, + vfiName: params.serviceAttached, + serviceUuid: this.route.snapshot.params.uuid, + contextType: this.route.snapshot.params.contextType, + flowType: 'default' + }) + .subscribe( + success => { + console.log(success); + this.store.mcUuid = success.vfcmt.uuid; + console.log(this.cleanProperty(success)); + this.store.cdump = success.cdump; + this.nodes = this.store.cdump.nodes; + this.store.setTabsProperties(this.nodes); + this.setDataFromMapToRuleEngine(success.cdump); + this.store.loader = false; + this.store.isEditMode = true; + }, + error => { + this.store.loader = false; + console.log(error.notes); + this.store.ErrorContent = Object.values(error.requestError); + this.store.displayErrorDialog = true; + } + ); + } + + updateCdump(cdump) { + this.store.cdump = cdump; + this.nodes = this.store.cdump.nodes; + this.store.setTabsProperties(this.nodes); + this.setDataFromMapToRuleEngine(cdump); + } + + importMC(params) { + console.log('importVfcmt: %o', params); + this.generalComponent.importCompleted = true; + this.store.loader = true; + this.restApi + .importVFCMT({ + name: params.name, + description: params.description, + templateUuid: params.template, + vfiName: params.vfni, + serviceUuid: this.route.snapshot.params.uuid, + contextType: this.route.snapshot.params.contextType, + flowType: params.flowType, + cloneVFCMT: params.isCloneVFCMT, + updateFlowType: params.isUpdateFlowType + }) + .subscribe( + success => { + console.log(success); + this.location.path(); + // this.location.go(); + this.store.mcUuid = success.vfcmt.uuid; + console.log(this.cleanProperty(success)); + this.store.cdump = success.cdump; + this.nodes = this.store.cdump.nodes; + this.store.setTabsProperties(this.nodes); + this.setDataFromMapToRuleEngine(success.cdump); + this.store.generalflow = 'edit'; + this.store.loader = false; + this.store.isEditMode = true; + }, + error => { + this.store.loader = false; + console.log(error.notes); + this.store.ErrorContent = Object.values(error.requestError); + this.store.displayErrorDialog = true; + } + ); + } + + setDataFromMapToRuleEngine(cdump) { + this.store.tabParmasForRule = cdump.nodes + .filter(x => x.name.includes('map')) + .map(y => { + return { + name: y.name, + nid: y.nid + }; + }); + } + + cleanProperty(response) { + return response.cdump.nodes.map(node => { + const t = node.properties.filter(item => + item.hasOwnProperty('assignment') + ); + node.properties = t; + return node; + }); + } + + saveCDUMP() { + debugger; + this.store.loader = true; + this.restApi + .saveMonitoringComponent({ + contextType: this.store.sdcParmas.contextType, + serviceUuid: this.store.sdcParmas.uuid, + vfiName: this.generalComponent.newVfcmt.vfni, + vfcmtUuid: this.store.mcUuid, + flowType: this.generalComponent.newVfcmt.flowType, + cdump: this.store.cdump + }) + .subscribe( + success => { + this.store.loader = false; + this.store.mcUuid = success.uuid; + this.toastr.success('', 'Save succeeded'); + }, + error => { + this.store.loader = false; + console.log(error.notes); + this.store.ErrorContent = Object.values(error.requestError); + this.store.displayErrorDialog = true; + }, + () => {} + ); + } + + saveAndCreateBlueprint() { + debugger; + this.store.loader = true; + if (this.store.cdumpIsDirty) { + this.restApi + .saveMonitoringComponent({ + contextType: this.store.sdcParmas.contextType, + serviceUuid: this.store.sdcParmas.uuid, + vfiName: this.generalComponent.newVfcmt.vfni, + vfcmtUuid: this.store.mcUuid, + cdump: this.store.cdump + }) + .subscribe( + success => { + this.store.loader = false; + this.store.mcUuid = success.uuid; + this.submitBlueprint(); + }, + error => { + this.store.loader = false; + console.log(error.notes); + this.store.ErrorContent = Object.values(error.requestError); + this.store.displayErrorDialog = true; + }, + () => {} + ); + } else { + this.submitBlueprint(); + } + } + + submitBlueprint() { + this.store.loader = true; + this.restApi + .submitMonitoringComponent({ + contextType: this.store.sdcParmas.contextType, + serviceUuid: this.store.sdcParmas.uuid, + vfiName: this.generalComponent.newVfcmt.vfni, + vfcmtUuid: this.store.mcUuid, + flowType: this.store.cdump.flowType + }) + .subscribe( + success => { + this.store.loader = false; + this.toastr.success('', 'Save succeeded'); + }, + error => { + this.store.loader = false; + console.log(error.notes); + this.store.ErrorContent = Object.values(error.requestError); + this.store.displayErrorDialog = true; + }, + () => {} + ); + } + + handleChange(e) { + this.store.setTabIndex(e.index - 1); + } +} diff --git a/public/src/app/router.animations.ts b/public/src/app/router.animations.ts new file mode 100644 index 0000000..072c031 --- /dev/null +++ b/public/src/app/router.animations.ts @@ -0,0 +1,66 @@ +import { + trigger, + state, + animate, + transition, + style, + query +} from '@angular/animations'; + +export const fadeAnimation = trigger('fadeAnimation', [ + transition('* => *', [ + query(':enter', [style({ opacity: 0 })], { optional: true }), + + query( + ':leave', + [style({ opacity: 1 }), animate('0.5s', style({ opacity: 0 }))], + { optional: true } + ), + + query( + ':enter', + [style({ opacity: 0 }), animate('0.5s', style({ opacity: 1 }))], + { optional: true } + ) + ]) +]); + +export const slideAnimation = trigger('slideAnimation', [ + transition('* <=> *', [ + // Initial state of new route + query( + ':enter', + style({ + position: 'fixed', + width: '100%', + transform: 'translateX(-100%)' + }), + { optional: true } + ), + // move page off screen right on leave + // query( + // ':leave', + // animate( + // '500ms ease', + // style({ + // position: 'fixed', + // width: '100%', + // transform: 'translateX(-100%)' + // }) + // ), + // { optional: true } + // ), + // move page in screen from left to right + query( + ':enter', + animate( + '500ms ease', + style({ + opacity: 1, + transform: 'translateX(0%)' + }) + ), + { optional: true } + ) + ]) +]); 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 + }); + }); + } +} diff --git a/public/src/app/rule-frame/rule-frame.component.html b/public/src/app/rule-frame/rule-frame.component.html new file mode 100644 index 0000000..10f3032 --- /dev/null +++ b/public/src/app/rule-frame/rule-frame.component.html @@ -0,0 +1,19 @@ +<div style="position: relative; display: flex; justify-content: flex-end; height: 100%;"> + + <div *ngIf="!tabName.includes('map')" style="margin: 1em;"> + <app-bar-icons [tabName]="tabName"></app-bar-icons> + </div> + + <!-- rule engine --> + <div style="width: 100%;" *ngIf="tabName.includes('map')"> + <app-slide-panel [activePane]="store.isLeftVisible ? 'left' : 'right'"> + <div leftPane style="height: 100%; overflow: auto;"> + <app-rule-list></app-rule-list> + </div> + <div rightPane style="height: 100%; overflow: auto;"> + <app-action-list></app-action-list> + </div> + </app-slide-panel> + </div> + +</div> diff --git a/public/src/app/rule-frame/rule-frame.component.scss b/public/src/app/rule-frame/rule-frame.component.scss new file mode 100644 index 0000000..2a95e01 --- /dev/null +++ b/public/src/app/rule-frame/rule-frame.component.scss @@ -0,0 +1,10 @@ +.frame { + display: block; + width: 100vw; + height: 100vh; + max-width: 100%; + margin: 0; + padding: 0; + border: 0 none; + box-sizing: border-box; +} diff --git a/public/src/app/rule-frame/rule-frame.component.ts b/public/src/app/rule-frame/rule-frame.component.ts new file mode 100644 index 0000000..4d5f999 --- /dev/null +++ b/public/src/app/rule-frame/rule-frame.component.ts @@ -0,0 +1,35 @@ +import { Component, OnDestroy, Input, ViewChild } from '@angular/core'; +import { Store } from '../store/store'; +import { BarIconsComponent } from '../bar-icons/bar-icons.component'; + +@Component({ + selector: 'app-rule-frame', + templateUrl: './rule-frame.component.html', + styleUrls: ['./rule-frame.component.scss'] +}) +export class RuleFrameComponent implements OnDestroy { + expandSetting = false; + configuration; + mappingTarget: string; + showHeaderBtn = true; + @Input() tabName: string; + // @ViewChild(BarIconsComponent) barFormsRef: BarIconsComponent; + + constructor(public store: Store) { + this.store.isLeftVisible = true; + } + + ngOnDestroy() {} + + onChangeMapping(configurationKey) { + console.log('changing ifrmae entry', configurationKey); + } + + isPropertyDdl(property) { + return property.hasOwnProperty('constraints'); + } + + enableSetting() { + this.expandSetting = !this.expandSetting; + } +} diff --git a/public/src/app/store/store.ts b/public/src/app/store/store.ts new file mode 100644 index 0000000..a9f2431 --- /dev/null +++ b/public/src/app/store/store.ts @@ -0,0 +1,98 @@ +import { Injectable } from '@angular/core'; +import { observable, computed, action, toJS, reaction } from 'mobx'; +import { findIndex } from 'lodash'; + +@Injectable() +export class Store { + @observable sdcParmas; + @observable isOwner; + @observable mcUuid; + @observable cdump; + @observable tabsProperties; + @observable tabIndex = 0; + @observable isEditMode = false; + @observable loader = false; + @observable cdumpIsDirty = false; + @observable expandAdvancedSetting = []; + @observable generalflow; + @observable vfiName; + // error dialog + @observable displayErrorDialog = false; + @observable ErrorContent = []; + + // rule-engine + @observable tabParmasForRule; + @observable ruleList = new Array(); + @observable ruleEditorInitializedState; + @observable isLeftVisible; + @observable inprogress; + + @action + updateRuleInList(rule) { + console.log('current list:', toJS(this.ruleList)); + console.log('new rule', rule); + const ruleIndex = findIndex(this.ruleList, function(ruleFromList) { + console.log( + `find match rule: list - ${ruleFromList.uid}, rule - ${rule.uid}` + ); + return ruleFromList.uid === rule.uid; + }); + if (ruleIndex > -1) { + console.log('update rule'); + this.ruleList[ruleIndex] = rule; + } else { + console.log('new rule'); + this.ruleList.push(rule); + } + } + + @action + updateRuleList(listOfRules) { + this.ruleList = listOfRules; + console.log(toJS(this.ruleList)); + } + + @action + removeRuleFromList(uid) { + this.ruleList = this.ruleList.filter(item => item.uid !== uid); + } + + @action + resetRuleList() { + this.ruleList = new Array(); + } + + @action + changeStateForEditor(data) { + this.ruleEditorInitializedState = data; + } + + @action + setTabIndex(value) { + this.tabIndex = value; + } + + @action + setTabsProperties(nodes) { + this.tabsProperties = nodes.map(tabItem => { + return tabItem.properties.map(x => { + if (!x.assignment) { + x.assignment = {}; + x.assignment.value = ''; + } else if (typeof x.assignment.value === 'object') { + x.assignment.value = JSON.stringify(x.assignment.value); + } + return x; + }); + }); + nodes.map(() => { + this.expandAdvancedSetting.push(false); + }); + console.log('tabsProperties: %o', this.tabsProperties.toJS()); + } + + @computed + get configurationForm() { + return this.tabIndex >= 0 ? this.tabsProperties[this.tabIndex] : null; + } +} |