summaryrefslogtreecommitdiffstats
path: root/public/src
diff options
context:
space:
mode:
Diffstat (limited to 'public/src')
-rw-r--r--public/src/app/api/feather-pipe.ts19
-rw-r--r--public/src/app/api/rest-api.service.spec.ts26
-rw-r--r--public/src/app/api/rest-api.service.ts179
-rw-r--r--public/src/app/app-routing.module.ts27
-rw-r--r--public/src/app/app.component.html7
-rw-r--r--public/src/app/app.component.scss20
-rw-r--r--public/src/app/app.component.ts31
-rw-r--r--public/src/app/app.module.ts109
-rw-r--r--public/src/app/bar-icons/bar-icons.component.html59
-rw-r--r--public/src/app/bar-icons/bar-icons.component.scss48
-rw-r--r--public/src/app/bar-icons/bar-icons.component.ts47
-rw-r--r--public/src/app/diagram/diagram.component.html19
-rw-r--r--public/src/app/diagram/diagram.component.scss28
-rw-r--r--public/src/app/diagram/diagram.component.spec.ts26
-rw-r--r--public/src/app/diagram/diagram.component.ts12
-rw-r--r--public/src/app/error-dialog/error-dialog.component.html17
-rw-r--r--public/src/app/error-dialog/error-dialog.component.scss0
-rw-r--r--public/src/app/error-dialog/error-dialog.component.ts17
-rw-r--r--public/src/app/general/general.component.html83
-rw-r--r--public/src/app/general/general.component.scss38
-rw-r--r--public/src/app/general/general.component.spec.ts55
-rw-r--r--public/src/app/general/general.component.ts323
-rw-r--r--public/src/app/home/home.component.html106
-rw-r--r--public/src/app/home/home.component.scss110
-rw-r--r--public/src/app/home/home.component.ts188
-rw-r--r--public/src/app/host/host.service.ts44
-rw-r--r--public/src/app/loader/loader.component.html4
-rw-r--r--public/src/app/loader/loader.component.scss152
-rw-r--r--public/src/app/loader/loader.component.spec.ts26
-rw-r--r--public/src/app/loader/loader.component.ts12
-rw-r--r--public/src/app/main/main.component.html62
-rw-r--r--public/src/app/main/main.component.scss33
-rw-r--r--public/src/app/main/main.component.ts228
-rw-r--r--public/src/app/router.animations.ts66
-rw-r--r--public/src/app/rule-engine/action-list/action-list.component.html100
-rw-r--r--public/src/app/rule-engine/action-list/action-list.component.scss77
-rw-r--r--public/src/app/rule-engine/action-list/action-list.component.ts290
-rw-r--r--public/src/app/rule-engine/action/action.component.html114
-rw-r--r--public/src/app/rule-engine/action/action.component.scss116
-rw-r--r--public/src/app/rule-engine/action/action.component.ts51
-rw-r--r--public/src/app/rule-engine/api/rule-engine-api.service.spec.ts19
-rw-r--r--public/src/app/rule-engine/api/rule-engine-api.service.ts134
-rw-r--r--public/src/app/rule-engine/condition/condition.component.html89
-rw-r--r--public/src/app/rule-engine/condition/condition.component.scss114
-rw-r--r--public/src/app/rule-engine/condition/condition.component.spec.ts51
-rw-r--r--public/src/app/rule-engine/condition/condition.component.ts161
-rw-r--r--public/src/app/rule-engine/confirm-popup/confirm-popup.component.html12
-rw-r--r--public/src/app/rule-engine/confirm-popup/confirm-popup.component.scss20
-rw-r--r--public/src/app/rule-engine/confirm-popup/confirm-popup.component.ts18
-rw-r--r--public/src/app/rule-engine/from/from.component.html70
-rw-r--r--public/src/app/rule-engine/from/from.component.scss63
-rw-r--r--public/src/app/rule-engine/from/from.component.ts91
-rw-r--r--public/src/app/rule-engine/host/exit-mode.enum.ts4
-rw-r--r--public/src/app/rule-engine/host/host-params.ts8
-rw-r--r--public/src/app/rule-engine/host/host.service.spec.ts18
-rw-r--r--public/src/app/rule-engine/host/host.service.ts56
-rw-r--r--public/src/app/rule-engine/rule-list/rule-list.component.html73
-rw-r--r--public/src/app/rule-engine/rule-list/rule-list.component.scss109
-rw-r--r--public/src/app/rule-engine/rule-list/rule-list.component.ts197
-rw-r--r--public/src/app/rule-engine/slide-panel/slide-panel.component.html8
-rw-r--r--public/src/app/rule-engine/slide-panel/slide-panel.component.scss15
-rw-r--r--public/src/app/rule-engine/slide-panel/slide-panel.component.ts27
-rw-r--r--public/src/app/rule-engine/target/target.component.html28
-rw-r--r--public/src/app/rule-engine/target/target.component.scss99
-rw-r--r--public/src/app/rule-engine/target/target.component.spec.ts57
-rw-r--r--public/src/app/rule-engine/target/target.component.ts77
-rw-r--r--public/src/app/rule-engine/target/target.util.ts50
-rw-r--r--public/src/app/rule-engine/target/target.validation.spec.ts83
-rw-r--r--public/src/app/rule-engine/version-type-select/version-type-select.component.html34
-rw-r--r--public/src/app/rule-engine/version-type-select/version-type-select.component.scss46
-rw-r--r--public/src/app/rule-engine/version-type-select/version-type-select.component.ts86
-rw-r--r--public/src/app/rule-frame/rule-frame.component.html19
-rw-r--r--public/src/app/rule-frame/rule-frame.component.scss10
-rw-r--r--public/src/app/rule-frame/rule-frame.component.ts35
-rw-r--r--public/src/app/store/store.ts98
-rw-r--r--public/src/assets/.gitkeep0
-rw-r--r--public/src/assets/fonts/OpenSans-Bold.ttfbin0 -> 224452 bytes
-rw-r--r--public/src/assets/fonts/OpenSans-BoldItalic.ttfbin0 -> 213168 bytes
-rw-r--r--public/src/assets/fonts/OpenSans-ExtraBold.ttfbin0 -> 222424 bytes
-rw-r--r--public/src/assets/fonts/OpenSans-ExtraBoldItalic.ttfbin0 -> 213336 bytes
-rw-r--r--public/src/assets/fonts/OpenSans-Italic.ttfbin0 -> 212760 bytes
-rw-r--r--public/src/assets/fonts/OpenSans-Light.ttfbin0 -> 222236 bytes
-rw-r--r--public/src/assets/fonts/OpenSans-LightItalic.ttfbin0 -> 213024 bytes
-rw-r--r--public/src/assets/fonts/OpenSans-Regular.ttfbin0 -> 217276 bytes
-rw-r--r--public/src/assets/fonts/OpenSans-SemiBold.ttfbin0 -> 221164 bytes
-rw-r--r--public/src/assets/fonts/OpenSans-SemiBoldItalic.ttfbin0 -> 212732 bytes
-rw-r--r--public/src/assets/images/Regex.svg12
-rw-r--r--public/src/assets/images/add-b.svg12
-rw-r--r--public/src/assets/images/add-s.svg12
-rw-r--r--public/src/assets/images/checkbox.pngbin0 -> 850 bytes
-rw-r--r--public/src/assets/images/close.svg12
-rw-r--r--public/src/assets/images/concat copy.svg12
-rw-r--r--public/src/assets/images/copy.svg12
-rw-r--r--public/src/assets/images/delete-b.svg12
-rw-r--r--public/src/assets/images/delete-s.svg12
-rw-r--r--public/src/assets/images/edit.svg12
-rw-r--r--public/src/assets/images/error404.svg12
-rw-r--r--public/src/assets/images/partial-error.svg12
-rw-r--r--public/src/assets/images/settings.svg12
-rw-r--r--public/src/assets/images/target.svg12
-rw-r--r--public/src/environments/environment.prod.ts5
-rw-r--r--public/src/environments/environment.ts10
-rw-r--r--public/src/favicon.icobin0 -> 5430 bytes
-rw-r--r--public/src/index.html17
-rw-r--r--public/src/jestGlobalMocks.ts33
-rw-r--r--public/src/main.ts12
-rw-r--r--public/src/polyfills.ts66
-rw-r--r--public/src/setupJest.ts2
-rw-r--r--public/src/stories/button.stories.ts33
-rw-r--r--public/src/stories/diagram.stories.ts22
-rw-r--r--public/src/stories/index.ts1
-rw-r--r--public/src/stories/loader.stories.ts6
-rw-r--r--public/src/stories/sdc-dropdown.stories.ts0
-rw-r--r--public/src/stories/select-autocomplete.stories.ts25
-rw-r--r--public/src/stories/treeSelect.stories.ts40
-rw-r--r--public/src/styles.css76
-rw-r--r--public/src/test.ts20
-rw-r--r--public/src/tsconfig.app.json10
-rw-r--r--public/src/tsconfig.spec.json20
-rw-r--r--public/src/typings.d.ts5
-rw-r--r--public/src/wallabyTest.ts14
121 files changed, 5621 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;
+ }
+}
diff --git a/public/src/assets/.gitkeep b/public/src/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/public/src/assets/.gitkeep
diff --git a/public/src/assets/fonts/OpenSans-Bold.ttf b/public/src/assets/fonts/OpenSans-Bold.ttf
new file mode 100644
index 0000000..7b52945
--- /dev/null
+++ b/public/src/assets/fonts/OpenSans-Bold.ttf
Binary files differ
diff --git a/public/src/assets/fonts/OpenSans-BoldItalic.ttf b/public/src/assets/fonts/OpenSans-BoldItalic.ttf
new file mode 100644
index 0000000..a670e14
--- /dev/null
+++ b/public/src/assets/fonts/OpenSans-BoldItalic.ttf
Binary files differ
diff --git a/public/src/assets/fonts/OpenSans-ExtraBold.ttf b/public/src/assets/fonts/OpenSans-ExtraBold.ttf
new file mode 100644
index 0000000..3660681
--- /dev/null
+++ b/public/src/assets/fonts/OpenSans-ExtraBold.ttf
Binary files differ
diff --git a/public/src/assets/fonts/OpenSans-ExtraBoldItalic.ttf b/public/src/assets/fonts/OpenSans-ExtraBoldItalic.ttf
new file mode 100644
index 0000000..8c4c15d
--- /dev/null
+++ b/public/src/assets/fonts/OpenSans-ExtraBoldItalic.ttf
Binary files differ
diff --git a/public/src/assets/fonts/OpenSans-Italic.ttf b/public/src/assets/fonts/OpenSans-Italic.ttf
new file mode 100644
index 0000000..e6c5414
--- /dev/null
+++ b/public/src/assets/fonts/OpenSans-Italic.ttf
Binary files differ
diff --git a/public/src/assets/fonts/OpenSans-Light.ttf b/public/src/assets/fonts/OpenSans-Light.ttf
new file mode 100644
index 0000000..563872c
--- /dev/null
+++ b/public/src/assets/fonts/OpenSans-Light.ttf
Binary files differ
diff --git a/public/src/assets/fonts/OpenSans-LightItalic.ttf b/public/src/assets/fonts/OpenSans-LightItalic.ttf
new file mode 100644
index 0000000..5ebe2a2
--- /dev/null
+++ b/public/src/assets/fonts/OpenSans-LightItalic.ttf
Binary files differ
diff --git a/public/src/assets/fonts/OpenSans-Regular.ttf b/public/src/assets/fonts/OpenSans-Regular.ttf
new file mode 100644
index 0000000..2e31d02
--- /dev/null
+++ b/public/src/assets/fonts/OpenSans-Regular.ttf
Binary files differ
diff --git a/public/src/assets/fonts/OpenSans-SemiBold.ttf b/public/src/assets/fonts/OpenSans-SemiBold.ttf
new file mode 100644
index 0000000..99db86a
--- /dev/null
+++ b/public/src/assets/fonts/OpenSans-SemiBold.ttf
Binary files differ
diff --git a/public/src/assets/fonts/OpenSans-SemiBoldItalic.ttf b/public/src/assets/fonts/OpenSans-SemiBoldItalic.ttf
new file mode 100644
index 0000000..8cad4e3
--- /dev/null
+++ b/public/src/assets/fonts/OpenSans-SemiBoldItalic.ttf
Binary files differ
diff --git a/public/src/assets/images/Regex.svg b/public/src/assets/images/Regex.svg
new file mode 100644
index 0000000..9665d32
--- /dev/null
+++ b/public/src/assets/images/Regex.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+ <title>icon/Regex</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="icon/Regex" fill="#666666">
+ <path d="M3.54146058,12.0499152 L1.86915752,12.0499152 L1.86915752,15.0314555 L1,15.0314555 L1,7.33005982 L3.55246258,7.33005982 C4.37394889,7.33005982 5.0138918,7.53359467 5.47231051,7.94067048 C5.93072921,8.34774628 6.15993512,8.92167789 6.15993512,9.66248252 C6.15993512,10.2199197 6.00957604,10.7003353 5.70885337,11.1037437 C5.40813071,11.5071522 4.99005912,11.7821993 4.45462607,11.9288933 L6.42398298,15.0314555 L5.37879356,15.0314555 L3.54146058,12.0499152 Z M1.86915752,11.2467696 L3.56346457,11.2467696 C4.09889762,11.2467696 4.5169692,11.1074124 4.81769187,10.8286939 C5.11841454,10.5499753 5.26877362,10.1649094 5.26877362,9.67348451 C5.26877362,9.18939436 5.12208184,8.81166301 4.82869387,8.54027914 C4.5353059,8.26889527 4.11356701,8.13320537 3.56346457,8.13320537 L1.86915752,8.13320537 L1.86915752,11.2467696 Z M8.08528405,12.4789929 C8.14396164,13.0731036 8.35483108,13.5425173 8.7178987,13.8872482 C9.08096631,14.231979 9.54854637,14.4043419 10.1206529,14.4043419 C10.4653838,14.4043419 10.7661019,14.3713362 11.0228164,14.3053239 C11.2795309,14.2393116 11.554578,14.1219582 11.847966,13.9532601 L12.1230158,14.5913758 C11.8076237,14.7820779 11.4867355,14.9214351 11.1603413,15.0094515 C10.8339472,15.0974679 10.4653841,15.1414755 10.054641,15.1414755 C9.48253441,15.1414755 8.98378234,15.0131201 8.55836979,14.7564057 C8.13295723,14.4996912 7.80656801,14.1494646 7.57919233,13.7057153 C7.35181665,13.261966 7.23813052,12.7540457 7.23813052,12.1819391 C7.23813052,11.5951632 7.3573176,11.0744073 7.59569532,10.619656 C7.83407305,10.1649046 8.15129403,9.81467799 8.54736779,9.56896557 C8.94344155,9.32325314 9.37984961,9.20039877 9.85660506,9.20039877 C10.3480299,9.20039877 10.7752697,9.31591855 11.1383373,9.54696158 C11.501405,9.77800461 11.781953,10.0952256 11.9799899,10.498634 C12.1780268,10.9020425 12.2770437,11.356787 12.2770437,11.8628813 C12.2770437,12.0682529 12.2697091,12.2736214 12.2550397,12.4789929 L8.08528405,12.4789929 Z M9.86760706,9.93753236 C9.3981863,9.93753236 9.00395213,10.1025606 8.68489272,10.4326221 C8.3658333,10.7626836 8.16963304,11.2064262 8.09628604,11.7638633 L11.4408922,11.7638633 L11.4408922,11.7088534 C11.4408922,11.3714572 11.3730472,11.0652381 11.2373553,10.7901869 C11.1016634,10.5151357 10.916465,10.3042662 10.6817546,10.1575722 C10.4470442,10.0108783 10.1756644,9.93753236 9.86760706,9.93753236 Z M17.183933,7 L18.0200845,7 L13.7513109,16.8357825 L12.9151594,16.8357825 L17.183933,7 Z M20.4955331,17.5179061 C19.7400591,17.5179061 19.0506077,17.3565452 18.4271583,17.0338184 L18.6471982,16.3516948 C18.9039126,16.4837194 19.1899616,16.5882373 19.5053537,16.6652516 C19.8207458,16.742266 20.1287985,16.7807725 20.4295212,16.7807725 C21.0603053,16.7807725 21.5205508,16.6340808 21.8102714,16.3406928 C22.099992,16.0473048 22.2448502,15.5852257 22.2448502,14.9544416 L22.2448502,14.3603339 C21.9514622,14.5950443 21.6599123,14.7655735 21.3701916,14.8719266 C21.080471,14.9782798 20.7522482,15.0314555 20.3855132,15.0314555 C19.9087577,15.0314555 19.4888525,14.9049339 19.1257849,14.6518867 C18.7627173,14.3988396 18.4840029,14.0522803 18.2896334,13.6121983 C18.0952638,13.1721164 17.9980805,12.6733643 17.9980805,12.1159271 C17.9980805,11.5364859 18.109933,11.0248983 18.3336413,10.581149 C18.5573497,10.1373997 18.8599015,9.7963413 19.2413058,9.55796357 C19.6227102,9.31958585 20.055451,9.20039877 20.5395411,9.20039877 C21.1849946,9.20039877 21.7717618,9.40576727 22.2998601,9.81651043 L22.3658721,9.32142071 L23.0699997,9.32142071 L23.0699997,14.8884296 C23.0699997,15.7465894 22.8517957,16.3993679 22.4153811,16.8467845 C21.9789665,17.2942012 21.3390235,17.5179061 20.4955331,17.5179061 Z M20.5725471,14.2943219 C20.8732698,14.2943219 21.1574851,14.2411462 21.4252016,14.134793 C21.6929181,14.0284399 21.9661316,13.8579107 22.2448502,13.6232003 L22.2448502,10.564646 C22.0174745,10.3666091 21.7680984,10.2125828 21.4967146,10.1025623 C21.2253307,9.99254178 20.9539509,9.93753236 20.682567,9.93753236 C20.1177952,9.93753236 19.6685516,10.1373999 19.3348228,10.537141 C19.001094,10.9368821 18.8342321,11.4631389 18.8342321,12.1159271 C18.8342321,12.7613807 18.9937594,13.2858038 19.3128188,13.6892123 C19.6318782,14.0926207 20.0517834,14.2943219 20.5725471,14.2943219 Z" id="Re/g"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/public/src/assets/images/add-b.svg b/public/src/assets/images/add-b.svg
new file mode 100644
index 0000000..25eb583
--- /dev/null
+++ b/public/src/assets/images/add-b.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+ <title>icon/add-b</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="icon/add-b" fill="#009FDB">
+ <path d="M25,22.9614258 L25,12 L23,12 L23,22.9614258 L12,22.9614258 L12,25 L23,25 L23,37 L25,37 L25,25 L37,25 L37,22.9614258 L25,22.9614258 Z M24,48 C10.745166,48 0,37.254834 0,24 C0,10.745166 10.745166,0 24,0 C37.254834,0 48,10.745166 48,24 C48,37.254834 37.254834,48 24,48 Z" id="Combined-Shape"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/public/src/assets/images/add-s.svg b/public/src/assets/images/add-s.svg
new file mode 100644
index 0000000..4487f54
--- /dev/null
+++ b/public/src/assets/images/add-s.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+ <title>icon/add-s</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="icon/add-s" fill="#009FDB">
+ <path d="M12.4761905,11.5238095 L12.4761905,6.76190476 L11.5238095,6.76190476 L11.5238095,11.5238095 L6.76190476,11.5238095 L6.76190476,12.4761905 L11.5238095,12.4761905 L11.5238095,17.2380952 L12.4761905,17.2380952 L12.4761905,12.4761905 L17.2380952,12.4761905 L17.2380952,11.5238095 L12.4761905,11.5238095 Z M12,22 C6.4771525,22 2,17.5228475 2,12 C2,6.4771525 6.4771525,2 12,2 C17.5228475,2 22,6.4771525 22,12 C22,17.5228475 17.5228475,22 12,22 Z" id="Combined-Shape"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/public/src/assets/images/checkbox.png b/public/src/assets/images/checkbox.png
new file mode 100644
index 0000000..e00c535
--- /dev/null
+++ b/public/src/assets/images/checkbox.png
Binary files differ
diff --git a/public/src/assets/images/close.svg b/public/src/assets/images/close.svg
new file mode 100644
index 0000000..5fe4cba
--- /dev/null
+++ b/public/src/assets/images/close.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+ <title>icon/close</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="icon/close" fill="#5A5A5A">
+ <path d="M20.1538462,11.8461538 L12.7692308,11.8461538 L12.7692308,4.46153846 C12.7692308,4.20769231 12.5615385,4 12.3076923,4 C12.0538462,4 11.8461538,4.20769231 11.8461538,4.46153846 L11.8461538,11.8461538 L4.46153846,11.8461538 C4.20769231,11.8461538 4,12.0538462 4,12.3076923 C4,12.5615385 4.20769231,12.7692308 4.46153846,12.7692308 L11.8461538,12.7692308 L11.8461538,20.1538462 C11.8461538,20.4076923 12.0538462,20.6153846 12.3076923,20.6153846 C12.5615385,20.6153846 12.7692308,20.4076923 12.7692308,20.1538462 L12.7692308,12.7692308 L20.1538462,12.7692308 C20.4076923,12.7692308 20.6153846,12.5615385 20.6153846,12.3076923 C20.6153846,12.0538462 20.4076923,11.8461538 20.1538462,11.8461538" id="close" transform="translate(12.307692, 12.307692) rotate(-315.000000) translate(-12.307692, -12.307692) "></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/public/src/assets/images/concat copy.svg b/public/src/assets/images/concat copy.svg
new file mode 100644
index 0000000..7906ae7
--- /dev/null
+++ b/public/src/assets/images/concat copy.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+ <title>icon/concat copy</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="icon/concat-copy" fill="#979797">
+ <path d="M14,14 L17.7458333,14 L17.7458333,6.25416667 L10,6.25416667 L10,10 L6.225,10 L6.225,17.775 L14,17.775 L14,14 Z M15,15 L17.8802083,15 C18.1753487,15 18.4357628,14.8914941 18.6614583,14.6744792 C18.8871539,14.4574642 19,14.2013904 19,13.90625 L19,6.11979167 C19,5.8246513 18.8871539,5.56423724 18.6614583,5.33854167 C18.4357628,5.11284609 18.1753487,5 17.8802083,5 L10.09375,5 C9.79860964,5 9.54253581,5.11284609 9.32552083,5.33854167 C9.10850586,5.56423724 9,5.8246513 9,6.11979167 L9,9 L6.09375,9 C5.79860964,9 5.54253581,9.11284609 5.32552083,9.33854167 C5.10850586,9.56423724 5,9.8246513 5,10.1197917 L5,17.90625 C5,18.2013904 5.10850586,18.4574642 5.32552083,18.6744792 C5.54253581,18.8914941 5.79860964,19 6.09375,19 L13.8802083,19 C14.1753487,19 14.4357628,18.8914941 14.6614583,18.6744792 C14.8871539,18.4574642 15,18.2013904 15,17.90625 L15,15 Z" id="Combined-Shape"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/public/src/assets/images/copy.svg b/public/src/assets/images/copy.svg
new file mode 100644
index 0000000..382aeca
--- /dev/null
+++ b/public/src/assets/images/copy.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+ <title>icon/copy</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="icon/copy" fill="#009FDB">
+ <path d="M16.6823207,17.779975 L16.6823207,8.13076534 L9.13076534,8.13076534 L9.13076534,17.779975 L16.6823207,17.779975 Z M16.6823207,6.77535796 C17.0480674,6.77535796 17.3707802,6.90982163 17.6504688,7.17875301 C17.9301575,7.44768439 18.0699997,7.76501866 18.0699997,8.13076534 L18.0699997,17.779975 C18.0699997,18.1457217 17.9301575,18.4684345 17.6504688,18.7481231 C17.3707802,19.0278117 17.0480674,19.167654 16.6823207,19.167654 L9.13076534,19.167654 C8.76501866,19.167654 8.44230584,19.0278117 8.16261721,18.7481231 C7.88292858,18.4684345 7.74308636,18.1457217 7.74308636,17.779975 L7.74308636,8.13076534 C7.74308636,7.76501866 7.88292858,7.44768439 8.16261721,7.17875301 C8.44230584,6.90982163 8.76501866,6.77535796 9.13076534,6.77535796 L16.6823207,6.77535796 Z M14.616938,4 L14.616938,5.38767898 L6.35540738,5.38767898 L6.35540738,15.0368886 L5,15.0368886 L5,5.38767898 C5,5.0219323 5.13446367,4.69921949 5.40339505,4.41953085 C5.67232643,4.13984222 5.9896607,4 6.35540738,4 L14.616938,4 Z" id="content_copy"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/public/src/assets/images/delete-b.svg b/public/src/assets/images/delete-b.svg
new file mode 100644
index 0000000..0d6575c
--- /dev/null
+++ b/public/src/assets/images/delete-b.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+ <title>icon/delete-b</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="icon/delete-b" fill="#959595">
+ <path d="M9,2 L9,3.979 L4,3.979 L4,7 L6.005,7 L6.005,22 L18.991,22 L18.991,7 L21,7 L21,3.979 L15.979,3.979 L15.979,2 L9,2 Z M10,4 L15,4 L15,3 L10,3 L10,4 Z M5,6 L20,6 L20,5 L5,5 L5,6 Z M7,21 L18,21 L18,7 L7,7 L7,21 Z M15,19 L16,19 L16,9 L15,9 L15,19 Z M12,19 L13,19 L13,9 L12,9 L12,19 Z M9,19 L10,19 L10,9 L9,9 L9,19 Z" id="Page-1"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/public/src/assets/images/delete-s.svg b/public/src/assets/images/delete-s.svg
new file mode 100644
index 0000000..a0bc142
--- /dev/null
+++ b/public/src/assets/images/delete-s.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+ <title>icon/delete-s</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="icon/delete-s" fill="#959595">
+ <path d="M10.001,5 L10.001,7.00009538 L7,7.00009538 L7.017,9 L8,9 L7.983,18.0000954 L17,18.0000954 L17,9 L18,9 L18,7.00009538 L15.001,7.00009538 L15.001,5 L10.001,5 Z M11.001,7 L14.001,7 L14.001,6 L11.001,6 L11.001,7 Z M14,16 L15,16 L14.983,10 L13.983,10 L14,16 Z M12,16 L13,16 L13,10 L11.983,10 L12,16 Z M10,16 L11,16 L10.983,10 L9.983,10 L10,16 Z" id="Page-1"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/public/src/assets/images/edit.svg b/public/src/assets/images/edit.svg
new file mode 100644
index 0000000..a93cd07
--- /dev/null
+++ b/public/src/assets/images/edit.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+ <title>icon/edit</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="icon/edit" fill="#0568AE">
+ <path d="M14.3676968,3 C14.34993,3.00162523 14.3322912,3.0044314 14.3148973,3.00839992 C14.1901219,3.02176437 14.0734683,3.07670988 13.9837006,3.16439835 L0.564235418,16.5154642 C0.476555486,16.6047554 0.421588128,16.7210325 0.408236985,16.8454609 L0.00264105915,20.3866253 C-0.0117307683,20.5335434 0.0329561816,20.6801411 0.126838313,20.7940604 C0.220720445,20.9079798 0.35607874,20.9798542 0.503036033,20.9938192 C0.542920833,20.998105 0.583150027,20.998105 0.623034827,20.9938192 L4.17259917,20.6026232 C4.30044091,20.5914939 4.42046286,20.5363602 4.51219576,20.4466247 L17.9220611,7.09555886 C18.0268567,6.99187563 18.0858246,6.8505817 18.0858246,6.7031628 C18.0858246,6.55574391 18.0268567,6.41444997 17.9220611,6.31076674 L14.7660928,3.17039829 C14.660086,3.06385873 14.5155836,3.00458679 14.3652968,3.00599994 L14.3652968,3.00599994 L14.3676968,3 Z M14.3760967,4.3439865 L16.7388729,6.69476289 L15.9540808,7.48435495 L13.5913046,5.13357857 L14.3760967,4.3439865 Z M12.7981125,5.91477072 L15.1608888,8.26554711 L4.31419775,19.0654386 L1.95142148,16.7134622 L12.7981125,5.91477072 Z M1.42822674,17.7646517 L3.25940834,19.5862334 L1.20142902,19.8118311 L1.42822674,17.7646517 Z M7.81936254,19.8814304 C7.51251621,19.8814304 7.26376812,20.1301785 7.26376812,20.4370248 C7.26376812,20.7438712 7.51251621,20.9926193 7.81936254,20.9926193 L23.4444056,20.9926193 C23.7512519,20.9926193 24,20.7438712 24,20.4370248 C24,20.1301785 23.7512519,19.8814304 23.4444056,19.8814304 L7.81936254,19.8814304 L7.81936254,19.8814304 Z" id="Shape"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/public/src/assets/images/error404.svg b/public/src/assets/images/error404.svg
new file mode 100644
index 0000000..24ccab6
--- /dev/null
+++ b/public/src/assets/images/error404.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="222px" height="186px" viewBox="0 0 222 186" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+ <title>icon/error404</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="icon/error404">
+ <image id="Page-1" x="0" y="0" width="222" height="186" xlink:href="data:image/png;base64,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"></image>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/public/src/assets/images/partial-error.svg b/public/src/assets/images/partial-error.svg
new file mode 100644
index 0000000..652075f
--- /dev/null
+++ b/public/src/assets/images/partial-error.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="190px" height="154px" viewBox="0 0 190 154" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+ <title>icon/partial-error</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="icon/partial-error">
+ <image id="Page-1" x="0" y="0" width="190" height="154" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAREAAADdCAYAAACcyUb1AAAABGdBTUEAA1teXP8meAAANJxJREFUeAHtfQt8VcWd/2/OuY8kQBLeIAJKElARLQaIoEB4BpSEqq22vmpbrXX73Ha7u91PH9Ztt/vf7up269Zqte22tbbVWiHySAQJQURERKGAAkFeKu+8Se7NvWf+37lw483NPTf3cc7NOefOfD7JPWdmzsz8vnPu98785je/YSSDRCBBBB4kUr5+4/UFInuXmpvrDvKc8KOaQgFF62wV9+8famyfvHu3P5wmP52NAHO2eFK6RBA4eUP5KBfzlqgKXcKJxjJGFzFiI4j4MCI2tPuTkTeR8i7kCeK5M7g+Q5zhU1yz0yj/GOfaUdwfpUCwYUeHdnheXV0giXJlVoshIEnEYh1iZnM+qCwflsfdVzOmTOGMpoAorkJ9V4A08sysN37ZvIs4NYBcdnLGdypc2aVxbVdhde178Z+TqVZBQJKIVXrC4Ha8UVrqnjh6+DRS+CwUPYsYm4HPiw2uxrTiOPFmkMsWYrRFC7J6n1/bNqq2tt20CmXBKSPgaBJpuG7GNK7yE8X12zB87hkexPz+zjkzytubOrdevXOnI17OszcsnOJyuZbgC7gY0s5kjA3oKbWN7zgPcGI7GGnrME9a+1aL/1U5DbJGfzqWRBqunzGRKewdzMMbgxRYWFL/xo4w5H8mUkvnlv0ew/lPaURPFm987b5wmp0+j86cmTtwWP5ihdESEMYytN02I410cRYjFby86zSN1vpYoHrkyvUn0i1TPp8aAo4lkf1lZflKDtsOAYsxLD4bJpJIAiGOoNHni17Z+uvU4Mv8U/uXFnuHuYqWgDhuw1C/EkQ4MPOtsFyNQehU6tCbf1LaA8/nr18vFLoyZAgBx5KIwO/AnOlYaVA3QMgiQSQ8qFWQi31TjEBCBEL090X1W3+aIazTqYY1VlWUK0T3gDiWo/2hZdZ0CnTus7yLc1qP1Z/fn+w6+FzJmgM+58pqDckcTSIC4h5EQoTpNKmCQDTGvo5pzP9Yoxtit+L44sUjcnLYPeikezFdKYmdS8bqIYApz1n8ePw2wAK/HLpi/R69fDI+PQQcTyICnj2zpo7PcXuhH6GQcRTXtIeLNr3+zfSgM+/pszdWzHap7CvE+Meh03GbV1MWlcxpMyft0Tdb/M9Jhayx/e54EhE6kGlzyp7GEudt3dAJHQnvWlSyafub3XH9fPEgVou+VrXk45iyfAt2G9f2c3McWz10J4dJ4//tZ21PDV+5OWRh61hhMySYo0kkWokKfcIPsUx4O4QO6UisQCRCUTrcPeFz0HN8A1OW4gz1e9ZXg6lOE96Dx8918Idhf3Iy6wFJAwAnkwhrmFv2h2glag8dCUYkXZo2f9Irr7+dBoYpPSqMwYrHDP0siOM7aOPYlAqRD6WNAMikjWnsUe7nPymoqTmbdoFZWIBjSWT/nGlTVeZ6M9YqTCSR4CV6pmjj1tsz1fdidFSxvOIO7Cf5PqYtEzJVr6wnPgJ4D2Ahyx8JdDU/MnTN1pb4uWVqJAKOJZHQVGbOjO+QRvuKXnn9mUihxfW7M68e43LnfCsY0H4z8dVtb0Wnm3HfVLV4kULKf2NadYUZ5csyDUCA02ms3X3/zVb/E1IBmxiejiWRxMTPTK7mqopiTFn+A+RxU2ZqlLUYgMCuYJC+OvjFtXUGlOXoIiSJmNi9sPMYkJfDvoeVoa+jGo+JVcmizUKA01+DfvrG4LVrD5lVhd3LVe0ugFXb33zjkgq3h1ZBcSr2tEicrdpRfbWL0eVM5fd9e2LxuZx9Da/XwRS2r0eyLV2ORAzu8eaKiiHkZQ9DafoZg4uWxfUzAjCnfy3A+b1Dq2t293NTLFW9JBEDu6Np+eJPKJw9iunLSAOLlUVZCwE/FK//BsvXH0nF6/mOkSRiwAt6srx8YE5+jlh1+bwBxckibICAGJXwYOCOwlXrDtqguaY2UZJImvA2Vy6YToobvkloYppFycdthgCIpAW2JV8uqK75nc2abmhzJYmkDidrqqr4Fvx6/FBukksdRCc8iU3hz/ip7f5s3YsjSSSFt/jM0rJ8t6fw/0Ae2GUrg0RAIMD3wgr5pvyVa9/NNjwkiSTZ46dvXHi5W1X/iqXbSUk+KrM7HAExvYGn+s8Mrq59weGi9hAPO89lSBSBpsqKW9yqa6skkEQRy658WNbPVxl7vmV5xY/EtotskV6ORBLsaSzf/jOWb/8Ny7cSswQxy+pssHRtPtV8x9gtWzqcjkPoC7F/9tThjLlKFJ/SMGHr1rhes/fOmDHUlUsT/ef4e5O3bTvudIA2lJe7SvNz/hfLt19wuqxSPmMRgMJ1a0cnr3K6vxIxncHJCp5K7C6dqnn5x8U2eT0oD5aVjXR7lZtVzq7x5ilVG4hcenmdEC/sP67J966UBOKE3sy8DBi0luXmKq+23LDA0cv/PXQiCjGQArshFpEIAtE8rEphPCs2kp2oWjDSm+/diBdhaeZfP1mjUxDAUL+Iu11bmisXOtblpSAR7uOBNRqjLtFxsYgkmkAwTAv6u/w184gceRBz4w3zx+eQSxDINU55maUc/YcA3EAMIUV9qWl5xcL+a4V5NXcrCffMLh3tUVyVCqeQd3GNcGyhj61iaoef1Lzl4RGIIBBfsGvNFa/uOGxes/qv5KYbF05gKs6qYWxc/7VC1uxIBDj5iGufyK+ufdFJ8nWTiBAqFpEQV7RsIZDQ3NXtxsFH2XMcpZNeZpvI4g9q2m1OsiXpQSKiE965bupFLpdnWXhEEu4Yx49AKhdfqihKPeTNmvNsw30rPzOOgJ8HgzcVvPjS6ozXbEKFPRSrovzLNu/4gPHAll51KXy7U6cwZ5aWX4zpSy1klgTSq+NlhAkIeKAjeRZHo84zoeyMF9lrJCJsRlRyL4dRlTeyNUJHEuzyvThpy9vvR8bb/frDpbOHD3DnbYC8k+0ui2x/bwTOBQI4q6q3MzJVUShX7V+jUjSrlZi2qGBF7dbeLbdPTA8SiSYQMYXhDFqR0NIvwXG6s4jk7MKFBWqe+rJchbHPC5tMS/c2NdEbp87oPjJ39CgaN3CAbnomEsQhWsGu4Jwhq9ftykR9ZtTRPZ2JRSBdjGow/KgW5CEqF2Siur3LxHELZjQmk2XunjzZ48pzPS8JJJOoZ64uMfrY3dgUt0I3RiP9HbD8W+hyu1aLKXV/tyXV+kMo6hHIZfWvvyd0JGIaE00kQgGbaqVWeG5s0difwRJ1vhXaIttgPAKHWtuoIxDULfiywgIanZerm57hhIvdnpzVYmSc4XoNqS5EIoy8N4Z1IGIKE+S8VhBIuAahB4kmEo/qXmrXnYpiM500ZQ/3rjM/9zY16wo2NMdLpcOG6qb3U8IUdYDrd3b8TgkSYQppoT0wYQKZ+Mq2Xn4jo4lEw76ZCaWlIRLqJ9BTqra5avGnQrtxU3paPmQHBE50dNBZny9mUz2YwswZNZIUC27GhoKysqKq4mcxG27hSEECHDsNX4QOe1cg4KuORSDh9gsiUXzsBZEXj62etn17yFQ+nG71z7NVC66CZucpuZ3f6j2VXvv2NuqPQmaOHEED3SGj7PQqMelp6OgeaKpccr9JxZtSbI/VGVNqsEihF1ZitqGTSizSJNkMExBo7eqiFYeOxDxhSuhBpg8fZkKthhfpD/Dg/CErX9pseMkmFGi76UiKGDAs5f5WEkiK6NnosXegC+ltFYI5O2TASgi9efpM6G/n2Ubya5iUWzN4XKT8GcewjrBm83q2ytH+QMKiQpH6L3iBqsL38tOZCAhSONDSGlM4QSzCbiQyCP2IGJ1YMjB2UW4OPQ1F65JbifSXmSzQeMePRM5WLrweDpd+YAGsZRNMRuBAcwsFEhxduEAgFw/IM7lF6RWPkfPCxcsXfyu9Usx/2tEkIo52UBVVHCzUv/bN5vdj1tcgjMviLetGA3TN0CGWVrCG2yt+AJtuXFwavrfip6NJxOUpfBTTmEusCLxsk7EICOMysU8mkTAyN5cmWXUa01sAD1PZ09CP9K99fu92dcc4lkSali2+DQRyV7ek8sLRCLwHEkkkiGnMrJHDE8lqmTyY1kzKy1EetkyDohriyCXekzeUj/K6cvbAnmhwlLzy1qEI7IM+ROhEwgHbNKjR5w/fdn+WjRhOEwvyu+/tdAFj0KUFK2vWWq3NjiSR5qqKP4G9odSWIVsRONnRSTXHenqtGIW9MovG2HfLF3b8HvI1+6aMqKtLbNiVoc533HSmZdniSkkgGXp7LFzNcZi+RwaxY3cWrFXtHIR+zzso5yGryeAoEjlVdd0gUpWfWw1k2Z7MI3D8XE8SmYYNdwNc9jeLwka3rzZVLZqWeUT1a3QUiXho4I8hqm39Muh3k0xJBoEAlntPdXZ2P3JRXh4V21QP0i3ERxcqln2fFCczfhTVv1eOIZGzNy6+EgoeW21c6t+ud27tJzGVCbtEFNOYmTZbjemzZxi7emq+574+82Uog2NIRFXZw9idaxl2zlD/yWpiIBA5lREb7vIcMI2JFhOjkYcay8sLo+P7494RX7qWqiVV2Fu1qD8AlHWaj8BpTE3adQzJCjweKsRfZPjwgj5kDMzai/IHRSY555rRMGVQzvcg0Df6WyjbL/G+UVrqLhkzbDdWZEr6G0xZv/EIxNvaL2obBN8gH79kXHfF8MpHzxw4SGIaUzV+LOU6cBTSLSzxLuoKXJm/ev2+j+Iyf2X76UzJRUO/KAkk8y9OpmrU29ofrj96E50KC8MrBhfS9fBe5mwCEQgwN7lcYjGhX4OtRyJHZ87MLRhecBBTmVH9iqLTK8cXkxUWkjJkGLHBQ4hhtYPwhzOLQ5JzsXPW78fJ8FgREZ+trcTPniHtzGkc+Z7YfpZYEIqt/X9577DuztyhXi8tGTvGkq4OY8ljShzMWDVO0wqra940pfwECrW1TqRg2KAvSQJJoJeTzMKGDCX1kktJGTuOlNEXhYgDv3hJloLsmFrw5ibip05R8NgR0g69R9r7x0LxiRT2LhwM6W3tF9OVOaOt6Ss1EdkMy4NhOI6G+gHKqzSszCQLsu1IROxqzMtlBzGks7cZYpIdZlZ2ZfgIUq/6GKmXXU6CREwLWH4NNhyg4J6/4XO/7khFLNE+f+iw7rEPgkDGDxxoWjPtVjCOmSsreLHm9f5odwo/L/3RzN515uTSVySB9MYlmRiGLfEh4gB5KKNGJ/No6nlFnVdOCf0RVl38tWso+PaOXuXFOzdGbOOXBBIFmUoPIWZJVGxGbm1JIkIXonDlm5jKyJACAiw/n1yzZpPr6qlEUcujKRSX+iPQafDjH8Z8Xs/B0BA8Y8EzY2LKkMlIaK0qGqsWTh28cl1vRja5IbYkkUHD8+8EgdjCbbfJ/ZdU8UIh6ppdTq5rsPUiFR1HUrX1nTm4dzdpJ473yhjv3BgxzdnwwUfEMwojmyuHSI8PAkSFVOFK8fZegJocYcclXuxBYl83GRdnFQ/Vm2t6GeV86evkmnGtJQiEuEZddS/HxDneuTFNWP0RxmThPz3HzDELdngkBuafhE/hj4xmMiSv7UYijTgjDKOQKzKEj+2rEQpT97LlpFw8NnVZoLvQTp4gfvoUafCYzlvh/KfjHI55x/ItznkhGHwxb05oaiT0LKxALAcPhcp7JK5je1MP7tpJXCwBRwVhXHasvT0qVv92ihyFfAQOtn0ozAVdIWXUubPtSARbGOUo5KPXJu6Va9oMci+Cri3ZqQvIQaygaAfxd/gQaSAPsVybSmBYQVHGjie1qJiUkonEBsIMPRikrvoNMYvry7gs8qGLBwxwrll7pKBJXGO5996T5eU/yKTjIluRyOnKxZdhRaY8CUyzMyuUpR6MPtTJUxKXHyShHT2MlZK3KPjOHuIRW+kTL6R3Tt7WRkL3If6wQTJke8IKBxNvbOyVOd65MdGZvfg1uXaEVItF4yLMAr2DPHcjPmN+dWxFIi7G7o0GTd73RECsvHg/Bb3zyASNeDEqCO7eRV2v1MecXvQsPc07QVRHDhOJvxhBuDTUMy6Lzj4Nu3Odb9YeLXVi90xRvoicGSMR2yyS7p482TO2aOwx6EOGJwZl9uUS+g/PHXcTG5SAI2J8oYV9RtemjcSbeo8K+gM94Uxo15mz5Is4gEooUNuE3iUijBs4gOaOTpAkI57LqsuAdl3+qtpXMyGzbUYiY4rGLpcEov9KCPN07x2fIYJis6+gwTaja3X1eRP0vjJnMB0jTZoKN4aRYcXhI5G35MV+nRnw2C5DHwio7B7kyAiJ2GaJF++XnMrovDcKVkESIhCh0FxXQ74nf2E5AoklmvAh0uLvOQqZIaYxFzb+xXpGxp1HgBO77YPK0oycE2qLkciJqgUjMe9aIF+Q3giIfS6eu+7pcwQipiz+vz5H2rGjvQuxaEykhzLRRGHqfskguV8mke7Cj25+nja0Enn/lEj+dPLYYiSSw9VbIOT5fefpSOuwZ4VNhvfTUKLmxT9hUXuvgXy/fMxWBCK6KpJEckLTGLkak8wrzBSWEetVW5AIY8qtyYCXFXnxU+O56ZN97rgVKy++Z35v2JJtJrGNPDtmBpZzBZHIkDgCcBKw5OzChbGt/RIvps+clieRUxUVo6FQnd2nJFmWwT1nHikw4IoXAtu2hqYwwrjLbqEZ5u3hA7rFFCahXbuSZKK72aPmMTGlMTVYnkQ8Hv4JIGD5dpraS1GFK+PGYyPdnKjYnrfBXW9TV83qlC1Ne5aW+bvwVCY0jYEyNZHgni99dUfjxEi9OTrO6HvrfzkVZZnRQtu6PGGNuhzvBbYh6oXgvnfIv+J52xKIkKv5gm2IOIBbLOvGC8IgX4V5v/uW24iBYGX4CAHO+KL3ysuxscm8oP8mmldnwiWLJSrGaW7CD2RBRvfc+TBs1t/6zrFRzv/8s7YmENGNU+BsuRwGZcKwLF4QBMKmXE2593+J1FGjyH373cRVWyw6xhPLsDSYwQ8cku9dYFiBMQqyNIkMYMPmQR/ijdHurIwSFqmhrfx60vt95HsOK3pRFp562a0cL0zax/ZBIKL9bMzFNPCfv0vswiZD19Xw0jZzlpVFy3zbGLvBzEotTSIQHFtQZQgjENqRCwfFesG/+kXz97/oVd4P8Rw2Mnk/+DEx7OYNBzU3L7RqRdL/ahgS8bkw8sboa/03Mqqm3dOnj9qATeVR0T1u35159RhEGLcfh7GlPSrI4htl/CVxV2OCe/eQUKZmS+DYaJj3gx+ROqy30tUtsCo3dQRvK5jxhZzYeMN805RFCZFIw+yyW3JzlQ/GzS17bffMyUOiEfwz9FoNc8qecntyj+Hz4ej0VO6bKhdfCuGLUnnWic+IJV3d4PNR19pVuslOSzivB/kYueCnJFYQUxv3Yvz+CN8lMoQQUFVPnBcoPZASIhHYNY2CLwh8sNIc96CXIonkQSy/ls4t+yVSPyeagjwXpdek80/D2k7ahlwAUnglUy65VBfW0E7ctlbddKcl4Lwm6thUR51/26UrmvsiHGoFZ9QynEeAK3yuWVgkRCJv1G/9BY4hekY0AkxyjSCSnddPGfwgCOSuuWVPgjg+K9LwC/G3IPd9WVynG/CemCZ0um3L9POu6fCLqhOEq8LAttd0Up0ZLX7OxKJv42M/012FEqMRV8US4hcUrs5EIgmpOItvWJREUdFZEyIR2JwHt2/ceheI5I+iAEEkA9S8l+6eU/brSALRNN/8kk07TkVXksq9wmhmKs857RnhoV29XN+lbODVV3QPgHIaFmF5GC6E2wDlyCFq27ghHN3r042VG3b55F7x2RgBuCaIjaxmyJ4QiYiKLxDJnfC1GdoVKKY2YJO7RZoYgRhJIOft/dkkUXa2B/XKq6BxEr+7McK5cxR4840YCc6PUvCtcOOv7U9P645GFJxwpi5YLN5PGYBArqbOMAOIhElEVC6I5HD91jtxebi7MThPGMRys1EjEFGuMkCdLj6668jiC3XylbrSB95+M+tGIZFgqCARduwonYtDpC4xEhkwMPKxrL3mCvU/iQB9Nm7Otb/A5/junsAWW2hT/9BQWmrYbkGFM/z8yiA8oytjxuoCEdixXTctGxLCo5H2VdW64rqH4uiKK/SJWPdBJyYw5RozxErm154dmHvtEyD/z4uGQEO+B8rPv4pr/B5Mo4GudUYRCez9p4hysz2oxSUhBVQsHLQPP4Bh2ZlYSVkVJ4gksP11CjY3x5RbKFiV6TPklAbo4DiJqTFBSjMyURJhB+eWPY7M94r6BIEwH83fXv/aJ3H7rIgzmEgkiQBT5VJ9MxlhXCYDMAKJuDRO5954XRcO18TLiCOfDGz0B5Xlva3z0gQmIRI5MLfsPtAE/hA43ysIZMLWrSdCOpKNrwnvSZFE8mgoX4r/hOEaCEl/OSLFcu34mDJ2nG6ztQP7dNOyKSG0UoOlPN/mel2x1cFDEj9CQ7cUZyTkMo/h362ESIRxrV1AKEYg5KN5gkDCkM6Dau+wIBJO+P6LTBTKG05P9nNJ1ZJiPJOb7HNOyy9OjtM7gpI6OkLHWjpN5lTlEUSi7d4tXtCYRbhgIs/GXxozLdsigZXha94JkUhR/banNb+/5MiRE6WRBBLuAEEkE+pf+1SA0xXb67d+KRyf4qdc2gVwbKj+qDPkbFnnC5Mi5rZ+TExp6Fwb+bBSEzMgXbnscqkXATgY5YsfaUODK9HSire8eaCPvHxi/Wt7+8iTSLLhQiZSqdXyKMOG6zZJO/Ghblo2JoBCMAdm5MdRnV6dKSDDpjxhLi+sXbM6MAZtvbEhoZGIsVXGLw1WsRPi58iOVIalSb2gwfGQDD0RCK3SHNjfMzLiTsHILvZkJyJTFlwybvz3y3Ikgn78yAYlCzpVT0RW2GuzdHfWWIdhdydm6YUYXwSPHNKVXoHBmfR4BrURM/77ZTkSwcugb12l+4o4L0GcKaMXeEtsmwi9/NkQL0iEw3ZGLyheL1GOqa5G9aq2VDx0IgMby8sLjWyU5UgEqh9DXAkYCVJ/lMXES68TOFZnZOiJgNB1xBuhMbebCJsZZQDZDvBcbCQOViMRECXpKwOMlNzqZXl0SATn09rxHBmz4RYjETj1Jq7jX1YRLgHgKV8GYaDHDd3NaykSaVmwQCgCLNWmfnvpvDovvCARGWIiIBZetC5/7DTshJZWq+ehYUwdEROkFCOt9YX1ylFIuB/FvkYZUkAgqMV+KNuXdnugoukbIfXIl9iNpd5UrjL9JYnE5HFMLo5jJGMGvRFKzMzZFSmmNHq+V4SNCMlR3PkXgjHnKlYZV/QtrLLr+4AXviu2xGKEIpSEMvRCQNiBKHrY4DxiDofWMgABzgYbiYO1RiKMDTJSODuXxdv1tyAx6WQndtdiyhJahYmRqolRSGdnjJQsjGLc0LVua5EIFuGysEtjiszjeG9n+Yb5f4pZt10j4x0vir1fRB3n7Cqaoe3GzM65JII1OjlOv/C68LP6DoeUOCbxhr5tNipMTGXUSy7VbbEmRnZy02IIH8yIdewHdOGLm2CpkQj8LMqRyIXu0k6f1u04hjN5ZeiJgFCcqpMu7xkZcRdsPAsTJBkuIKDj+Ts1fCxFIpociXT3Ij+jTyLKGHFaqQyRCIRGIkX6nuACH7wvd/CGAeNkqOdqS5FIWEb5CaOp06cw/I5t86CMws4AeShTj9dEkIh7gr4XicC+d+VIpAdixt1IEjEOS2NLgiKQnzwZu0wQiAr/GDJ8hAAfNIjcej5YMNUJ7tgmSeQjuAy9shSJ4KgIHeMIQ2W2TWHBI4d126oUleimZVuCGIW4rp+LzTOxtR5dwhP88RNyOmPSi2EpEsEpWHINLqKjtQZ9JzuqOJRJ50sTUURWXGoYaXium6Mrq+9gAzadAS7dHNmVAGsaHXPo1HCwFInAkk6ORCL6MXj4kK6pNoPzYUU6Hw6hxbE71zPpsgjkel76ceB5yA9rz+isvcO5Tob6krAUieCXQo5EIl9t6EWC+/dFxvS4dk0t7XGfjTchhWrlx0nR2eYvdvV2ra+VW8MjXg54XmmNuE370lokwlhb2hI5rIDg7p26EqlXTKZst14VUxnvoqW6GHW8+w4pHZ1SHxKBENcUQ3+sLUUinAd0liMiEMiyyyCWJnm7DrcqCrmunZVliPQUV51+LblHj+4ZGXF3bs2L5IJCROpDIkBReFPEXdqXliIRFuRn05bIaQVg92nw7bd0pXKVTic2KF833ckJoVHIXffoihhobSVt00Y5lemFEHMuibSpQUkivTocutWtW/RdIsJmxD1/YYynnB+lli8kz7jxuoK2b6ojYd+d9WfNRCPENf2NWdF5E7i31Ejkieo6QSKxzTQTEMapWcSO3uCut3XFU6dcTcrF2eUkX4MiNeeez+kuc2vwtXru9/9HLiyDy6lMz1eHa/x4z5j07ixFIg8KAuEkRyMx+rQLw3JdB834oniWLcc2VjgjzoIgVmQ89z1ALhzUrRfaNtWTC9MZubTbGyGNM0NPP7MUiVwQV//wkN54ZE0Mb2qkwPZtuvKKnb3uRRW66U5KYFdMoZzyBboiCd8hbU8+Rm6pUI2JkRpghn7HLEciOEbzSEzJZSQFNr4Mxzr6dkKu6WUUsmR1MFYcXt1yv/mPuh7MhOjNq1aSW45CYr4F+H61FdTUGDratxyJQPKjMaWXkcTh3s9fuyYuEmJaw4Y48+gejmMfcv75u+TCubp6oQt+Qzp+85QchegABP2Q4d8vy5EIDhw+qCO/jAYCwZ1vkdZwQB8LHBXp/fSdcO80QD+PDVPEmTGeL36FPFdOidv6sw//hDxwoSB1IbFhgm1eQ+yU1GMtRyJBYvq7zlKX01FP+lf+FRsE9I0OxUjEc8fdxBxy9mzItP3Oe8i7YJHuaozo4Jb6OuI7tssVmfhve5xfoPgP6qVajkQ0JSBJRK+3LsSLJV//yufj+gxVRo0mD754bIDNRyQYgbhuv4tylt9MDBa6esEPd5Ktj/wneVVFjkL0QEI8dCL6m7HiPBcvSb9X4j1lYtrO5uA+SCoPCOkDY7ExLwBjqnhBGX0Ree+5l9hgQ48ZiVeloWlCB+L+wt9R7s23EovjyU3YhJz+3rfJG+wiVdqFxO0D7JvZGzdDComWI5F5dXUBbFU2XNAUsLH8I10YvsczQhMCiKmN9577SIlj2WlJQTGC8n7jn8hbcUNcAhEe3E898hNyHT1MbkkgfXalz6/t6TNTkhksRyKh9nOmb56ZpICOzo4vkL/6BdIOvRdXTDZwIHnv+iy5Z5fH1SnELSSDiezSCZTz0L+Td9b1cacwokln/u9XpNVvIA+mOlKZ2kcncX5iVG2t4ZtcLUkiWIbS3//eB05Zl4wNev4//4G094/FF13s+C2fT16hcLXqEjCmLK4bKyn3e/9K7qLiPgmvEQpm33N/pBxMeySBxO9+kcqJdvSdK/kcliQRjbRdyYuSvU+IM2b9f/gtaXF8sobRUS4topz7v0TuufMs5TFemTiJvN//IfbD3EdqHHP2sBzNL9VQ++M/pxyQo9SDhFHp45PT9j5ypJTsSukpkx9iPradvBiry+2XCSMtDNF8IBLPLbeSWjIp/nPiF3/OPFKv+hh1bdxwXq8CuPsjKBeNIRdWXlwzryM1weNBG//yLLX/6gnKw0qMCySCkasMCSCgKZopJGJZ/JurKt4Bh/TxbUgAuWzLAuWie9EScpXNTFhy3oh9Oa9vCZEJj2NWn3CBfWVEG9XiEnJBaarCH0oiI49QkSC600/+gjpfeF4SSF8Yx0jv7OocPWJ1naE7eEU11iWR5RW/xibue2JgIaMSQEDsofHA9yh5vQnkvpAlEKDggX0hq9igsIrFvWEBxKGMHEXKlVeRC1MpsVqkFhQmXLyGkdbJn/yYtK2vhqYwcgSSMHShjLAPOVSwoubS5J5KLLclpzOi6WC3enzcI67tFH63P3Wr4rtKigwTNbh3N3WeOE6emz5BYsqQUMA0R73sitCfIBANy6ZBrPwIXQtHWUL3knAQyk4ocJWx40gpmUgM5KEKEsG+F4a0ZEIn2nDyB98h96mTlItn09WBWKWPksEg7bw89H1Ku5hYBViWRLRAcKMSx8AoljAyricC/OwZ8v3mSXItuYHc066FIlUlHoTPp0T0Hx6VWNFEcuFPBKGeEu4IODa48dYWCk17sOU+FIReQnhbz82F4+h8YqPHEBuGTXK5eaQMHESKsJrF80kHtLMJO3Jbn3iMcrAfxnNhFSaFkpKu2mkP4MBz8aNsSrAsiRSuWncQepH38PKaMgQzBc2oQv9ud2wL459PPv/FDKeH7we9EH+HblTxjr7tamuj5r17Qv5jh3z7u+R2u41bxr37jhB2X/jt06HPJy7ch/sjGthw/0TH2+wefiTMCZYlkQvirsXnA+aIbn6pDnn5zAcqogZhwt6CE+v8H35AHoxEc7CJUMUIJBMLdU7tL4xC3iusro1vkRjRB8leWppEoFi1NYkk2xnZnF+DDqYVug/fsWOhXbgDoBB2gUSEEVkmCMTh2IvvkWnB0iTS2dL5ck5+jg9a1iSWGEzDKumCHT48ThqPWA8E4NKg7egR8n34IciDKA+6FRd0N6rQs6SiR4lVSYJxDu6vVQlCkFI2S5PIiLq6tpaqJZsh2fyUpOvnh5w6PE4X1pAn9pMnqAPEEWxuwrTFTQP6kTzC8ji0vzpaTrWYpg8R2FmaREQDMZ+rxi+SLUlEtF8G9KGmkQ9k4TtzhvwwbNPg/1SFE2U3pit5eXkhnUd/jDyyoW/w/Xlp7JYtHWbKankS6eJdz+LM9/8CCIqZQJhRtoOHx73gEqMLodcQB2gHO30U6DhHgfZ26mptI+1cG8HtJVaY1dAyrSsXylJMVxShMEVJmZ629Gr8hQgn9hcW5uG9ytxgiyV3TGk24W273lwojCk9HUOmJePsfQCVUGGInSzipRIKUQw2QoZhIk4VaUJJijRBGuKzv0I6fWSkQaD58vMuLdA+snDVK41m1mX5kYgQnjN6Fi+dLUgknc7Kd9uiO3RFDBNDJFFEXus+KBNMQQCDv1qzCUQ0PNzvpghhVKGnKipGe3PYMZRnuymNURjIciQCySKA4zLvKKiu+UOyzyWb3xZfyuE1NR9Cw7oxWeFkfolAtiKAUUhrh4+vyIT8tiARAQRAeTITgMg6JAJOQABTjGfgCrE9E7LYhkROBhr+gu3MZzIBiqxDImB3BDjvytiPrm1IpGTNAR+cRP7O7p0r2y8RMB0BTjsLqtfrn/5ucANsQyJC7mCQP2Ww/LI4iYDjEOBcezyTQtmKRIasqv0blCP1mQRI1iURsBMCmPK3aa1+01dkIjGxFYmIhgc5PRwpgLyWCEgEIhFgvx5cV9cUGWP2te1I5KfVNdXYD3DAbGBk+RIB2yHAeSCoBf4z0+22HYk8SKRhSvNIpoGS9UkErI4ADv14fkj1uiNGtPPArKtGvHPdpEHxynrnuqkXbbjkkhxbWKxGC3J05szcguEFR2BvC0eeMkgEJAICAa4FZhZUr3stXTT2zJo6Psfl+TLK6cRBck8U1287GlUm2z+7rApboGZjI9QR241EhDBiazNY96dRgslbiUD2IsDpZSMIRADoVZTQSWZw6Z2LvdZfODBneuTO0I8IBHmxO5vZkkSEoIGuxv+RxmcCCRkkAkAgqH3XKByKXtl+JKCx50AO8LF/nkj2l5VdLMrvHoHgGtOY09Qe+I0tpzNhsJoqK/5JUdi/h+/lp0QgGxHAj2kNDqZaYrTs+68ruxYuX24BkYjRBkb/fC/Otr1G1MM4naFzgZ8Xbd/ebNuRiBCk08cfBVmeFNcySASyFoEgfc8M2Us2b30tGKS/hEcksQhE1GtrEgltMOLs/5kBoCxTImAHBKC8qC54seZ1s9oqiERjtD+yfE3TVooRSDjO1iQihDjRdeB/xbkaYYHkp0QgixAIBgPav5gp78G50yuxZ+38aWsXKlJV5VPvzry6+2xW25OI2JgHNwHfMhNIWbZEwJIIcP6r0FYQkxonCETjylxRvNCBME1bG57auD0594eJxPYkIgQsrK75C3QjdeJaBolANiCAH86WzoDPFF2IwK/h+hnLwgSiMH62pdX3WNEr29bBbuSvISLhlBcmEkeQiBA62KX9vfgQ1zJIBJyOALy4PzRidd1xM+RsmFNWwhVWLsoWBNLc7P/51LfeCu3HgeHZqxoPvhAmEpc7907HkMjg1S+9BYv4jDliMaPzZJkSgYQQ4LRn3/un/yehvClkYj5qgXd0n7ADaQt2PBYmkHBRxZve2BwakTBsQdH4SVvbiYSFCn82lpcXqvne3TDFvSgcJz8lAo5CAKsIgSDNHbKqZpOZcr1RWup+cfv24INir5pO2F9c7C05cMDvKBIRssIA7RYYoD2nI7eMlgjYGwFOT+WvXHuvlYRwzHQmDGpIycoz4+U6XKf8lAhkBAHOP9CCbZZbiXQciYjO9GtdX4KJbrcxTEY6WFYiETAdAfZAJg6jSlYMR5LIsBdffh8zua8kC4bMLxGwKgL4UfwNpjErrdg+R5KIABonf/0OwP/JiqDLNkkEkkEANiEH/bztq8k8k8m8jiURASIPtD8AIol2qJJJfGVdEoH0EMBiDDHt9uErN7emV5B5TzuaRMT8UeP0GcCnu0xlHrSyZIlA+gjgMPt/LVhRuzX9kswrwdEkImAbvLJmAw42/qF5EMqSJQJmIcA31ayo+ZFZpRtVrmpUQVYu55p9DZuKJhXNYIwVW7mdsm0SgW4EOD/h89HCGQ0NLd1xFr1w/EhE4H6r2FPjozukywCLvoWyWT0REEapXLtteE3Nhz0TrHmXFSQioC+oqTkLLfcncNlhza6QrZIInEcAjob+YXD1SxvtgkfWkIjoEFizvolzSu/DuTXoJxkkAtZDQNP4rwtW1tjqJIOsIhHxyhSsrH0aFPKv1nt9ZIuyHgEc+/D+wWNftBsOjtuAl2AHsOaqJb/D4Tt3JJhfZpMImI3ArkB7YPaQdetst10j60YiF94Efqzh6OfgO/Jls98MWb5EoE8EOJ0KaIFlRhDI/lmlRe9eP+PqPuuMyPBuaekwHFA1awORKyI64ctsJRGavHu3P3AucDOQ2pUwWjKjRMBgBMRGUc60SiPO0G2YM+N21e1+160qb8E72S/Q1D5nGvvmTJ/jGuBqUJi6edycstoHUzgBImtJRLwLgvm7/J03iL0J4l4GiUCGEfDDEPJm4yxS2efR/pDtF2yi7j84t+xx3OsSSYhAmLIaefOF3Pic96nrppWI62RCVpOIAGromrpj2GVTganN+8kAJ/NKBNJEwB/U+K2F1bWGTanBFq/2bBO7T49IDs6dMdsFAgF1DAg/g1HRsbPa8SPh+0Q/s55EBFBYUjuAjwVY+j2RKHAyn0QgDQSCcE56x+DqmhVplNHr0bPtgYdgvYCTDyIDiCRqaiMIBOSxJopATsPCbdmsLceStqPSHepENiNbrk/fuPByj6qukz5as6XH+0VOPzaF3lm4cu2zZtS+e/JkT87QgX/A1OSWyPJBLo8X1W99AARyvQ6BLJz0yutvRz6T6LUkkSikmpbOL2Iez3oAMz4qSd5KBNJDgPNOjHY/mV9d+2J6BcV/+gKRPAMiEQsH3QFE8jx+IBfj3R4YjsQU5jSOW1k08dVtOC0htSBJJAZuZysXjlOZug6dkLSSKUZxMkoiAO6gc5zx5YUratZlAg49IulZNz8T6NIWpkMgojxJIj1R7b47VVEx2ptDtYDoyu5IeSERSAEBsYwb1ILLsIz7SgqPp/zIeSIZ9EcYVd7UuxBjCESUKxWrvdENxYgdlFqgfY40SNMBSEYnhACmEEeCAX59pglENE7YQmH087OYDeVsPUYgKelAosuTJBKNSMS98Ix2tOHoUrwIv42IlpcSgYQQwBRmh5+0mWYeuh2vIcIKFQdxvxAzD6NbYZAmCCbt2UjaBcRsoPMiWXNlxYMYFn5XWOQ4TzwpkdEIYJv4al9z520j6urajC47kfJCBELKGryuIUOy0DPYwo73t8fAAStFjxbXvyacQKe8s71HgYk0LkvzcHiP/z7mtncJBVmWYiDFThABfFUfqVmxtqpfCSTCEjXUbE6NwSBdh/e3OlIMhdGXD8699qeIS/nHUZJIJKJ9XAs3AvD3MAsdIc3k+8AqG5Mx7W3Hj/2nC6rXfiPkTa8fQNg/d8ZMDDZWM2IF3dWDQPDeLirZvPW1zjOtn8CQI3qJ+SvpEEnK7NPdwCy8aK6oGEJeehpDxSVZKL4UOQYCIJADQQrcMmTl+p0xkjMStW/W9I+pbqWuB4EQb9KCtLD4la3bw40IrdoMG/QXfPmXhePOf/KfTNi49R97xvV9J0cifWPUK4dwtfjIypobsXnqISQGe2WQEdmFAM5+1lp80/uTQATgqkt9qC8CEfnEqk3n6dZbQHyrxH134PQP0KWM7b5P8EKSSIJARWd7EGfZCD1JgAfnYnh4ODpd3mcBApx8XNO+mr+y5qbBdXVNFpA4wjM8b+IBvihyBBLZvhCRnGm7GUSCTXjnA2fMDx/RneH7RD8liSSKlE6+IStf2hxsD1wNPckfdbLIaCciwGlngLpmFFTXimXSlFc2jITGx7u+BeW/eA/XwhJ1XtHm19+IV/4FIrkJjX8EZLKJk/bpkk07TsV7Jlaa1InEQiXFOCwD3wXzvf/GkHJIikXIx6yOgDjWkujhE10N3ytZc8Bn9eZmon2SRAxG+UTVgpG55P4ZFsw+aXDRsrh+RgC/8m9h+fbz4tSAfm6KpaqXJGJSd7RULalC0T8HmYwxqQpZbOYQ6ID/j4d2NPv/c15dnRiJyBCBgCSRCDCMvjy7cGGBmqs8hKXgv4OlYEpOcI1ukywvOQSgK1jLAoGv5a9evy+5J7MntySRDPT1meULrnBx139Ju5IMgG1QFVA27sP+/a/B691ag4p0bDGSRDLYtS3LFldyhT0MMinOYLWyqiQQgN6jCZvWfni04djPxOpFEo9mbVZJIhnu+jdKS93FY4Z+FkTyHaziJG3Yk+HmZk11mLa0Y2vlo9QW+En++vVnskZwAwSVJGIAiKkUsX9psXeEp/h+/Op9G8rXUamUIZ8xAAG4LMTU5fFOCvx45Mr10lF3CpBKEkkBNCMf+aCyNG8gG/YAZ/Q1OTIxEtn4ZcE48BzI+6mAv/M/zh8bEj+/TNVHQJKIPjYZTRHTnJIxQ24jpnwTZPKxjFaeTZXhyEpMXR47Fzj36Og1m5K2zswmqBKVVZJIokhlMF/T8oqFmOZ8DXqTpag2dKJZBqt3ZFVQmG6HodgvGlt9v7+0ri7p/SGOBMUgoSSJGASkGcWcWbx4rCuHPgd/MZ8DoYwzow4nl4kpSyte8Ge0oPZE4ara7q3wTpa5P2STJNIfqCdZ558xGqlYtqiCFOUzIJRlWEXIS7KIbMquYT9cvabR0/5W3x/7y7tYNgEuScRmvX188eIBuTlUyUiB8yy+FJawOTYTwfjmQskBcn0Vyuk/+zv5s8JTv/GVyBL1EJAkooeMDeLPLC3LV9WCpUxlSxhnN2K1YbgNmm1IEy+srtTBqnRNoJOvGFpbe9SQgmUhSSMgSSRpyKz5wIM4Q+jvKxeUEnMtwepOBVo5DaTitWZrU2qVmKbs4Rp7iVNwbWNrV71UkKaEo+EPSRIxHFJrFCiM2UYqE0o1hc2GDmUWCOU6kMtQa7QuoVZ0YJSxDSOOTWj7Frgf3GwR72EJNT6bMkkSyaLevnDG8BScijZF2KJgCjSFGMd5w8zdbzBAnwG3fEdAGLtAdjuhEH07oAV37WoP7Jfb7vutV5KqWJJIUnA5L/OG8nLX1FxlPFM8xVzl42CfcjGUteND+hVOo0Eyw4izQnzBByUrPWwzcHATOwPngTDq4qfFNZSfH+KlO4p6DgWC2sHmdv8BOS1JFllr5ZckYq3+sGxrQmQzIDAowDx5Lq54maoqPMi7zzYJKNTu0jQ4+tWC8Pfbcko7fk66D7RsdxrasP8PdfAGoLBSg7EAAAAASUVORK5CYII="></image>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/public/src/assets/images/settings.svg b/public/src/assets/images/settings.svg
new file mode 100644
index 0000000..8d60b5a
--- /dev/null
+++ b/public/src/assets/images/settings.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+ <title>icon/settings</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="icon/settings" fill="#999999">
+ <path d="M12,15.4042105 C10.1203158,15.4042105 8.59505263,13.8804211 8.59505263,12 C8.59505263,10.1210526 10.1203158,8.59652632 12,8.59652632 C13.8796842,8.59652632 15.4049474,10.1210526 15.4049474,12 C15.4049474,13.8804211 13.8796842,15.4042105 12,15.4042105 L12,15.4042105 Z M19,13.2835789 L19,10.7178947 L17.432,10.4548421 C17.3111579,10.0318947 17.1438947,9.62884211 16.9338947,9.25231579 L17.8571579,7.95768421 L16.0430526,6.14357895 L14.7476842,7.06684211 C14.3718947,6.85684211 13.9688421,6.68884211 13.5458947,6.56873684 L13.2828421,5 L10.7171579,5 L10.4541053,6.56873684 C10.0311579,6.68884211 9.62884211,6.85684211 9.25231579,7.06684211 L7.95694737,6.14357895 L6.14284211,7.95768421 L7.06610526,9.25231579 C6.85610526,9.62884211 6.68884211,10.0311579 6.568,10.4548421 L5,10.7178947 L5,13.2835789 L6.568,13.5466316 C6.68884211,13.9695789 6.85610526,14.3718947 7.06610526,14.7484211 L6.14284211,16.0430526 L7.95694737,17.8578947 L9.25231579,16.9346316 C9.62884211,17.1446316 10.0311579,17.3118947 10.4541053,17.432 L10.7171579,19.0007368 L13.2835789,19.0007368 L13.5458947,17.432 C13.9688421,17.3118947 14.3718947,17.1446316 14.7476842,16.9346316 L16.0430526,17.8578947 L17.8578947,16.0437895 L16.9346316,14.7491579 C17.1446316,14.3726316 17.3118947,13.9695789 17.4327368,13.5466316 L19,13.2835789 Z" id="Fill-1-Copy-15"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/public/src/assets/images/target.svg b/public/src/assets/images/target.svg
new file mode 100644
index 0000000..d7199ec
--- /dev/null
+++ b/public/src/assets/images/target.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+ <title>icon/target</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="icon/target" fill="#979797">
+ <path d="M7,8 L18,8 L18,9 L7,9 L7,8 Z M7,12 L18,12 L18,13 L7,13 L7,12 Z M7,16 L18,16 L18,17 L7,17 L7,16 Z" id="Combined-Shape"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/public/src/environments/environment.prod.ts b/public/src/environments/environment.prod.ts
new file mode 100644
index 0000000..bc3ac4b
--- /dev/null
+++ b/public/src/environments/environment.prod.ts
@@ -0,0 +1,5 @@
+export const environment = {
+ production: true,
+ apiBaseUrl: 'dcae/dcaeProxy',
+ imagePath: 'dcae_fe/assets/images'
+};
diff --git a/public/src/environments/environment.ts b/public/src/environments/environment.ts
new file mode 100644
index 0000000..7a68ddd
--- /dev/null
+++ b/public/src/environments/environment.ts
@@ -0,0 +1,10 @@
+// The file contents for the current environment will overwrite these during build.
+// The build system defaults to the dev environment which uses `environment.ts`, but if you do
+// `ng build --env=prod` then `environment.prod.ts` will be used instead.
+// The list of which env maps to which file can be found in `.angular-cli.json`.
+
+export const environment = {
+ production: false,
+ apiBaseUrl: 'http://localhost:8446',
+ imagePath: './assets/images'
+};
diff --git a/public/src/favicon.ico b/public/src/favicon.ico
new file mode 100644
index 0000000..8081c7c
--- /dev/null
+++ b/public/src/favicon.ico
Binary files differ
diff --git a/public/src/index.html b/public/src/index.html
new file mode 100644
index 0000000..4821f7e
--- /dev/null
+++ b/public/src/index.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+ <meta charset="utf-8">
+ <title>DcaeFe</title>
+ <base href="/">
+
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
+</head>
+
+<body>
+ <app-root></app-root>
+</body>
+
+</html>
diff --git a/public/src/jestGlobalMocks.ts b/public/src/jestGlobalMocks.ts
new file mode 100644
index 0000000..e93276f
--- /dev/null
+++ b/public/src/jestGlobalMocks.ts
@@ -0,0 +1,33 @@
+global['CSS'] = null;
+
+const mock = () => {
+ let storage = {};
+ return {
+ getItem: key => (key in storage ? storage[key] : null),
+ setItem: (key, value) => (storage[key] = value || ''),
+ removeItem: key => delete storage[key],
+ clear: () => (storage = {})
+ };
+};
+
+Object.defineProperty(window, 'localStorage', { value: mock() });
+Object.defineProperty(window, 'sessionStorage', { value: mock() });
+Object.defineProperty(document, 'doctype', {
+ value: '<!DOCTYPE html>'
+});
+Object.defineProperty(window, 'getComputedStyle', {
+ value: () => {
+ return {
+ display: 'none',
+ appearance: ['-webkit-appearance']
+ };
+ }
+});
+Object.defineProperty(document.body.style, 'transform', {
+ value: () => {
+ return {
+ enumerable: true,
+ configurable: true
+ };
+ }
+});
diff --git a/public/src/main.ts b/public/src/main.ts
new file mode 100644
index 0000000..91ec6da
--- /dev/null
+++ b/public/src/main.ts
@@ -0,0 +1,12 @@
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule)
+ .catch(err => console.log(err));
diff --git a/public/src/polyfills.ts b/public/src/polyfills.ts
new file mode 100644
index 0000000..d68672f
--- /dev/null
+++ b/public/src/polyfills.ts
@@ -0,0 +1,66 @@
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE9, IE10 and IE11 requires all of the following polyfills. **/
+// import 'core-js/es6/symbol';
+// import 'core-js/es6/object';
+// import 'core-js/es6/function';
+// import 'core-js/es6/parse-int';
+// import 'core-js/es6/parse-float';
+// import 'core-js/es6/number';
+// import 'core-js/es6/math';
+// import 'core-js/es6/string';
+// import 'core-js/es6/date';
+// import 'core-js/es6/array';
+// import 'core-js/es6/regexp';
+// import 'core-js/es6/map';
+// import 'core-js/es6/weak-map';
+// import 'core-js/es6/set';
+
+/** IE10 and IE11 requires the following for NgClass support on SVG elements */
+// import 'classlist.js'; // Run `npm install --save classlist.js`.
+
+/** IE10 and IE11 requires the following for the Reflect API. */
+// import 'core-js/es6/reflect';
+
+
+/** Evergreen browsers require these. **/
+// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
+import 'core-js/es7/reflect';
+
+
+/**
+ * Required to support Web Animations `@angular/platform-browser/animations`.
+ * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
+ **/
+// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
+
+
+
+/***************************************************************************************************
+ * Zone JS is required by default for Angular itself.
+ */
+import 'zone.js/dist/zone'; // Included with Angular CLI.
+
+
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
diff --git a/public/src/setupJest.ts b/public/src/setupJest.ts
new file mode 100644
index 0000000..1d3bd02
--- /dev/null
+++ b/public/src/setupJest.ts
@@ -0,0 +1,2 @@
+import 'jest-preset-angular';
+import './jestGlobalMocks';
diff --git a/public/src/stories/button.stories.ts b/public/src/stories/button.stories.ts
new file mode 100644
index 0000000..80411e8
--- /dev/null
+++ b/public/src/stories/button.stories.ts
@@ -0,0 +1,33 @@
+import { storiesOf } from '@storybook/angular';
+import { action } from '@storybook/addon-actions';
+import { boolean, text } from '@storybook/addon-knobs/angular';
+import { MatButtonModule, MatIconModule } from '@angular/material';
+
+storiesOf('Button', module)
+ .add('Basic', () => ({
+ template: `
+ <div style="margin:2em;">
+ <button mat-raised-button color="primary" (click)="onClick($event)">primary</button>
+ <button mat-raised-button color="primary" [disabled]="disabled">disabled</button>
+ <button mat-raised-button class="btn-secondry"> {{ btnText }} </button>
+ </div>
+ `,
+ props: {
+ disabled: boolean('disabled', true),
+ btnText: text('btnText', 'secondry'),
+ onClick: action('click')
+ },
+ moduleMetadata: {
+ imports: [MatButtonModule]
+ }
+ }))
+ .add('Round', () => ({
+ template: `
+ <button mat-mini-fab style="background-color:#009FDB">
+ <mat-icon class="material-icons">add</mat-icon>
+ </button>
+ `,
+ moduleMetadata: {
+ imports: [MatButtonModule, MatIconModule]
+ }
+ }));
diff --git a/public/src/stories/diagram.stories.ts b/public/src/stories/diagram.stories.ts
new file mode 100644
index 0000000..00dd922
--- /dev/null
+++ b/public/src/stories/diagram.stories.ts
@@ -0,0 +1,22 @@
+import { storiesOf } from '@storybook/angular';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { DiagramComponent } from '../app/diagram/diagram.component';
+import { array } from '@storybook/addon-knobs/angular';
+
+storiesOf('Diagram', module).add('simple', () => ({
+ component: DiagramComponent,
+ moduleMetadata: {
+ imports: [],
+ schemas: [NO_ERRORS_SCHEMA],
+ declarations: [],
+ providers: []
+ },
+ props: {
+ list: array('list', [
+ { source: 'node1dsvsdsvd', target: 'node2' },
+ { source: 'node3', target: 'node4' },
+ { source: 'node5', target: 'nodedsvsds6' },
+ { source: 'node7', target: 'node8' }
+ ])
+ }
+}));
diff --git a/public/src/stories/index.ts b/public/src/stories/index.ts
new file mode 100644
index 0000000..5c1c18f
--- /dev/null
+++ b/public/src/stories/index.ts
@@ -0,0 +1 @@
+declare module '*.md';
diff --git a/public/src/stories/loader.stories.ts b/public/src/stories/loader.stories.ts
new file mode 100644
index 0000000..38d6f90
--- /dev/null
+++ b/public/src/stories/loader.stories.ts
@@ -0,0 +1,6 @@
+import { storiesOf } from '@storybook/angular';
+import { LoaderComponent } from '../app/loader/loader.component';
+
+storiesOf('Loader', module).add('simple loader', () => ({
+ component: LoaderComponent
+}));
diff --git a/public/src/stories/sdc-dropdown.stories.ts b/public/src/stories/sdc-dropdown.stories.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/public/src/stories/sdc-dropdown.stories.ts
diff --git a/public/src/stories/select-autocomplete.stories.ts b/public/src/stories/select-autocomplete.stories.ts
new file mode 100644
index 0000000..104f0b1
--- /dev/null
+++ b/public/src/stories/select-autocomplete.stories.ts
@@ -0,0 +1,25 @@
+import { storiesOf } from '@storybook/angular';
+import { action } from '@storybook/addon-actions';
+import { boolean, text, array } from '@storybook/addon-knobs/angular';
+import { NgSelectModule } from '@ng-select/ng-select';
+
+storiesOf('select-autocomplete', module).add('select', () => ({
+ template: `
+ <ng-select [items]="cities"
+ bindLabel="name"
+ bindValue="id"
+ placeholder="Select city"
+ [(ngModel)]="selectedCityId">
+ </ng-select>
+ `,
+ props: {
+ cities: array('cities', [
+ { id: 1, name: 'Vilnius' },
+ { id: 2, name: 'Kaunas' },
+ { id: 3, name: 'PabradÄ—' }
+ ])
+ },
+ moduleMetadata: {
+ imports: [NgSelectModule]
+ }
+}));
diff --git a/public/src/stories/treeSelect.stories.ts b/public/src/stories/treeSelect.stories.ts
new file mode 100644
index 0000000..9d34c6b
--- /dev/null
+++ b/public/src/stories/treeSelect.stories.ts
@@ -0,0 +1,40 @@
+import { storiesOf } from '@storybook/angular';
+import { TreeModule } from 'angular-tree-component';
+import { MatButtonModule, MatIconModule } from '@angular/material';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { TargetComponent } from '../app/rule-engine/target/target.component';
+
+storiesOf('Target', module).add('target component', () => ({
+ component: TargetComponent,
+ moduleMetadata: {
+ imports: [
+ TreeModule,
+ MatButtonModule,
+ MatIconModule,
+ BrowserAnimationsModule
+ ]
+ },
+ props: {
+ nodes: [
+ {
+ name: 'commonEventHeader',
+ children: [
+ {
+ name: 'domain',
+ children: null,
+ isRequired: true,
+ requiredChildren: null,
+ id: 'event.commonEventHeader.domain'
+ },
+ {
+ name: 'eventId',
+ children: null,
+ isRequired: true,
+ requiredChildren: null,
+ id: 'event.commonEventHeader.eventId'
+ }
+ ]
+ }
+ ]
+ }
+}));
diff --git a/public/src/styles.css b/public/src/styles.css
new file mode 100644
index 0000000..4b1d433
--- /dev/null
+++ b/public/src/styles.css
@@ -0,0 +1,76 @@
+/* You can add global styles to this file, and also import other style files */
+@import '@angular/material/prebuilt-themes/indigo-pink.css';
+
+@font-face {
+ font-family: 'Open Sans';
+ src: url('./assets/fonts/OpenSans-SemiBold.ttf') format('ttf');
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ /* font-family: 'Open Sans', sans-serif;
+ font-size: 13px; */
+}
+
+html,
+body,
+app-root,
+app-home {
+ height: 100%;
+ font-family: 'Open Sans', sans-serif;
+ font-size: 13px;
+ /* height: 100vh; */
+}
+
+div,
+span {
+ font-weight: 400;
+}
+
+div .field-label,
+span .field-label,
+div .field-label > span {
+ font-weight: 600;
+}
+
+/* form input validation border */
+textarea.ng-touched.ng-invalid:not(form),input.ng-touched.ng-invalid:not(form) {
+ border: 1px solid #cf2a2a !important;
+}
+
+/** reset button **/
+.mat-fab.mat-primary,
+.mat-mini-fab.mat-primary,
+.mat-raised-button.mat-primary {
+ background-color: #009fdb;
+}
+
+.mat-fab,
+.mat-mini-fab,
+.mat-raised-button {
+ box-shadow: none !important;
+}
+
+.btn-secondry {
+ border: 1px solid #009fdb !important;
+ color: #009fdb !important;
+ background: #ffffff !important;
+ box-shadow: none !important;
+}
+
+/* Astrix required */
+.required::before {
+ content: '*';
+ color: red;
+ margin-right: 3px;
+}
+
+/** overide dialog **/
+.ui-dialog {
+ border-top: solid 6px #cf2a2a;
+}
+.ui-dialog-titlebar {
+ background: white;
+}
diff --git a/public/src/test.ts b/public/src/test.ts
new file mode 100644
index 0000000..1631789
--- /dev/null
+++ b/public/src/test.ts
@@ -0,0 +1,20 @@
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'zone.js/dist/zone-testing';
+import { getTestBed } from '@angular/core/testing';
+import {
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting
+} from '@angular/platform-browser-dynamic/testing';
+
+declare const require: any;
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting()
+);
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().map(context);
diff --git a/public/src/tsconfig.app.json b/public/src/tsconfig.app.json
new file mode 100644
index 0000000..5ed576b
--- /dev/null
+++ b/public/src/tsconfig.app.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/app",
+ "baseUrl": "./",
+ "module": "es2015",
+ "types": ["node"]
+ },
+ "exclude": ["test.ts", "**/*.spec.ts"]
+}
diff --git a/public/src/tsconfig.spec.json b/public/src/tsconfig.spec.json
new file mode 100644
index 0000000..63d89ff
--- /dev/null
+++ b/public/src/tsconfig.spec.json
@@ -0,0 +1,20 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/spec",
+ "baseUrl": "./",
+ "module": "commonjs",
+ "target": "es5",
+ "types": [
+ "jasmine",
+ "node"
+ ]
+ },
+ "files": [
+ "test.ts"
+ ],
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/public/src/typings.d.ts b/public/src/typings.d.ts
new file mode 100644
index 0000000..ef5c7bd
--- /dev/null
+++ b/public/src/typings.d.ts
@@ -0,0 +1,5 @@
+/* SystemJS module definition */
+declare var module: NodeModule;
+interface NodeModule {
+ id: string;
+}
diff --git a/public/src/wallabyTest.ts b/public/src/wallabyTest.ts
new file mode 100644
index 0000000..a0a2899
--- /dev/null
+++ b/public/src/wallabyTest.ts
@@ -0,0 +1,14 @@
+import './polyfills';
+
+import 'zone.js/dist/zone-testing';
+
+import { getTestBed } from '@angular/core/testing';
+import {
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting
+} from '@angular/platform-browser-dynamic/testing';
+
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting()
+);