support-multiple-ric-instances 25/1825/23
authorNicolas Hu <jh245g@att.com>
Wed, 27 Nov 2019 17:40:27 +0000 (12:40 -0500)
committerjh245g <jh245g@att.com>
Fri, 20 Dec 2019 20:58:51 +0000 (15:58 -0500)
 Add instance-selector service
 Revise ac-xapp, app-mgr, caas-ingress, e2-mgr services to support multiple ric instances
 Revise ac-xapp, app-control, catalog, caas-ingress, ran-control components to listen to the instance change

Change-Id: I4773581fc6e3936b1100e14557bd0db217ffed25
Signed-off-by: Jun (Nicolas) Hu <jh245g@att.com>
26 files changed:
docs/release-notes.rst
webapp-frontend/src/app/ac-xapp/ac-xapp.component.ts
webapp-frontend/src/app/app-configuration/app-configuration.component.html
webapp-frontend/src/app/app-configuration/app-configuration.component.ts
webapp-frontend/src/app/app-control/app-control.component.ts
webapp-frontend/src/app/app-control/app-control.datasource.ts
webapp-frontend/src/app/caas-ingress/caas-ingress.component.ts
webapp-frontend/src/app/caas-ingress/caas-ingress.datasource.ts
webapp-frontend/src/app/catalog/catalog.component.ts
webapp-frontend/src/app/catalog/catalog.datasource.ts
webapp-frontend/src/app/interfaces/dashboard.types.ts
webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts
webapp-frontend/src/app/ran-control/ran-control.component.ts
webapp-frontend/src/app/ran-control/ran-control.datasource.ts
webapp-frontend/src/app/rd.component.html
webapp-frontend/src/app/rd.component.scss
webapp-frontend/src/app/rd.component.ts
webapp-frontend/src/app/rd.module.ts
webapp-frontend/src/app/services/ac-xapp/ac-xapp.service.ts
webapp-frontend/src/app/services/app-mgr/app-mgr.service.ts
webapp-frontend/src/app/services/caas-ingress/caas-ingress.service.ts
webapp-frontend/src/app/services/common/common.service.spec.ts [new file with mode: 0644]
webapp-frontend/src/app/services/common/common.service.ts [new file with mode: 0644]
webapp-frontend/src/app/services/e2-mgr/e2-mgr.service.ts
webapp-frontend/src/app/services/instance-selector/instance-selector.service.spec.ts [new file with mode: 0644]
webapp-frontend/src/app/services/instance-selector/instance-selector.service.ts [new file with mode: 0644]

index ddcd11a..bb43e4d 100644 (file)
@@ -5,7 +5,7 @@
 RIC Dashboard Release Notes
 ===========================
 
-Version 2.0.0, 22 Dec 2019
+Version 2.0.0, 20 Dec 2019
 --------------------------
 * Change application properties file to YAML format
 * Add mock list of RIC instances to application properties
@@ -13,6 +13,9 @@ Version 2.0.0, 22 Dec 2019
 * Drop AUX cluster container list fetched via CAAS Ingress
 * Revise controller methods to require RIC instance key
 * Upgrade to Spring-Boot 2.1.11.RELEASE
+* Add instance-selector service
+* Revise ac-xapp, app-mgr, caas-ingress, e2-mgr service to support multiple ric instances
+* Revise ac-xapp, app-control, catalog, caas-ingress, ran-control components to listen to the instance change
 
 Version 1.3.0, 26 Nov 2019
 --------------------------
index 1e0df3b..f85f18e 100644 (file)
  * ========================LICENSE_END===================================
  */
 
-import { Component, OnInit } from '@angular/core';
-import { FormGroup, FormControl, Validators } from '@angular/forms';
-import { ACAdmissionIntervalControl, ACAdmissionIntervalControlAck } from '../interfaces/ac-xapp.types';
+import { HttpErrorResponse } from '@angular/common/http';
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { FormControl, FormGroup, Validators } from '@angular/forms';
+import { ACAdmissionIntervalControl } from '../interfaces/ac-xapp.types';
 import { ACXappService } from '../services/ac-xapp/ac-xapp.service';
 import { ErrorDialogService } from '../services/ui/error-dialog.service';
 import { NotificationService } from './../services/ui/notification.service';
-import { HttpErrorResponse } from '@angular/common/http';
+import { Subscription } from 'rxjs';
+import { InstanceSelectorService } from '../services/instance-selector/instance-selector.service';
 
 @Component({
   selector: 'rd-ac-xapp',
@@ -34,16 +36,20 @@ import { HttpErrorResponse } from '@angular/common/http';
 export class AcXappComponent implements OnInit {
 
   private acForm: FormGroup;
+  private instanceChange: Subscription;
+  private instanceKey: string;
 
   constructor(
     private acXappService: ACXappService,
     private errorDialogService: ErrorDialogService,
-    private notificationService: NotificationService) { }
+    private notificationService: NotificationService,
+    public instanceSelectorService: InstanceSelectorService, ) { }
 
   ngOnInit() {
     const windowLengthPattern = /^([0-9]{1}|[1-5][0-9]{1}|60)$/;
     const blockingRatePattern = /^([0-9]{1,2}|100)$/;
     const triggerPattern = /^([0-9]+)$/;
+
     this.acForm = new FormGroup({
       // Names must match the ACAdmissionIntervalControl interface
       enforce: new FormControl(true, [Validators.required]),
@@ -51,18 +57,28 @@ export class AcXappComponent implements OnInit {
       blocking_rate: new FormControl('', [Validators.required, Validators.pattern(blockingRatePattern)]),
       trigger_threshold: new FormControl('', [Validators.required, Validators.pattern(triggerPattern)])
     });
-    // TODO: show pending action indicator
-    this.acXappService.getPolicy().subscribe((res: ACAdmissionIntervalControl) => {
-      this.acForm.controls['enforce'].setValue(res.enforce);
-      this.acForm.controls['window_length'].setValue(res.window_length);
-      this.acForm.controls['blocking_rate'].setValue(res.blocking_rate);
-      this.acForm.controls['trigger_threshold'].setValue(res.trigger_threshold);
-      // TODO: clear pending action indicator
-    },
-      (error: HttpErrorResponse) => {
-        // TODO: clear pending action indicator
-        this.errorDialogService.displayError(error.message);
-      });
+
+    this.instanceChange = this.instanceSelectorService.getSelectedInstancekey().subscribe((instanceKey: string) => {
+      if (instanceKey) {
+        // TODO: show pending action indicator
+        this.instanceKey = instanceKey;
+        this.acXappService.getPolicy(instanceKey).subscribe((res: ACAdmissionIntervalControl) => {
+          this.acForm.controls['enforce'].setValue(res.enforce);
+          this.acForm.controls['window_length'].setValue(res.window_length);
+          this.acForm.controls['blocking_rate'].setValue(res.blocking_rate);
+          this.acForm.controls['trigger_threshold'].setValue(res.trigger_threshold);
+          // TODO: clear pending action indicator
+        },
+          (error: HttpErrorResponse) => {
+            // TODO: clear pending action indicator
+            this.errorDialogService.displayError(error.message);
+          });
+      }
+    })
+  }
+
+  ngOnDestroy() {
+    this.instanceChange.unsubscribe();
   }
 
   updateAc = (acFormValue: ACAdmissionIntervalControl) => {
@@ -74,7 +90,7 @@ export class AcXappComponent implements OnInit {
         blocking_rate: +acFormValue.blocking_rate,
         trigger_threshold: +acFormValue.trigger_threshold
       };
-      this.acXappService.putPolicy(acFormValueConverted).subscribe(
+      this.acXappService.putPolicy(this.instanceKey, acFormValueConverted).subscribe(
         response => {
           if (response.status === 200) {
             this.notificationService.success('AC update policy succeeded!');
index 28d6eac..755e4e6 100644 (file)
   -->
 
 <div mat-dialog-title *ngIf="loading$ | async">
-  Loading {{data.name}} Configuration
+  Loading {{data.xapp.name}} Configuration
   <mat-spinner diameter=70></mat-spinner>
 </div>
 
 <div *ngIf="((loading$ | async)==false) " class="config-div">
 
   <div mat-dialog-title class="config-title">
-    {{data.name}} Configuration
+    {{data.xapp.name}} Configuration
   </div>
 
   <div  class="config-form">
index 3c7a95c..3e539aa 100644 (file)
@@ -53,13 +53,13 @@ export class AppConfigurationComponent implements OnInit {
   xappLayout: any;
   ngOnInit() {
     this.loadingSubject.next(true);
-    this.appMgrService.getConfig()
+    this.appMgrService.getConfig(this.data.instanceKey)
       .pipe(
         finalize(() => this.loadingSubject.next(false))
       )
       .subscribe(
         (allConfig: any) => {
-          this.loadConfigForm(this.data.name, allConfig)
+          this.loadConfigForm(this.data.xapp.name, allConfig)
         }
       );
   }
@@ -71,8 +71,8 @@ export class AppConfigurationComponent implements OnInit {
       config: event,
       layout: this.xappLayout
     }
-    this.loadingDialogService.startLoading("Updating " + this.data.name + " configuration");
-    this.appMgrService.putConfig(config)
+    this.loadingDialogService.startLoading("Updating " + this.data.xapp.name + " configuration");
+    this.appMgrService.putConfig(this.data.instanceKey, config)
       .pipe(
         finalize(() => {
           this.loadingDialogService.stopLoading();
index c97a195..35770dc 100644 (file)
@@ -21,15 +21,17 @@ import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
 import { Component, OnInit, ViewChild } from '@angular/core';
 import { MatSort } from '@angular/material/sort';
 import { Router } from '@angular/router';
+import { Subscription } from 'rxjs';
+import { finalize } from 'rxjs/operators';
 import { XappControlRow } from '../interfaces/app-mgr.types';
 import { AppMgrService } from '../services/app-mgr/app-mgr.service';
+import { InstanceSelectorService } from '../services/instance-selector/instance-selector.service';
 import { ConfirmDialogService } from '../services/ui/confirm-dialog.service';
 import { ErrorDialogService } from '../services/ui/error-dialog.service';
 import { LoadingDialogService } from '../services/ui/loading-dialog.service';
 import { NotificationService } from '../services/ui/notification.service';
 import { AppControlAnimations } from './app-control.animations';
 import { AppControlDataSource } from './app-control.datasource';
-import { finalize } from 'rxjs/operators';
 
 @Component({
   selector: 'rd-app-control',
@@ -41,7 +43,9 @@ export class AppControlComponent implements OnInit {
 
   displayedColumns: string[] = ['xapp', 'name', 'status', 'ip', 'port', 'action'];
   dataSource: AppControlDataSource;
-  @ViewChild(MatSort, {static: true}) sort: MatSort;
+  @ViewChild(MatSort, { static: true }) sort: MatSort;
+  private instanceChange: Subscription;
+  private instanceKey: string;
 
   constructor(
     private appMgrSvc: AppMgrService,
@@ -49,48 +53,59 @@ export class AppControlComponent implements OnInit {
     private confirmDialogService: ConfirmDialogService,
     private errorDialogService: ErrorDialogService,
     private loadingDialogService: LoadingDialogService,
+    public instanceSelectorService: InstanceSelectorService,
     private notificationService: NotificationService) { }
 
+
   ngOnInit() {
     this.dataSource = new AppControlDataSource(this.appMgrSvc, this.sort, this.notificationService);
-    this.dataSource.loadTable();
+    this.instanceChange = this.instanceSelectorService.getSelectedInstancekey().subscribe((instanceKey: string) => {
+      if (instanceKey) {
+        this.instanceKey = instanceKey;
+        this.dataSource.loadTable(instanceKey);
+      }
+    })
+  }
+
+  ngOnDestroy() {
+    this.instanceChange.unsubscribe();
   }
 
   controlApp(app: XappControlRow): void {
     // TODO: identify apps without hardcoding to names
-    const acAppPattern0 =  /[Aa][Dd][Mm][Ii][Nn]/;
-    const acAppPattern1 =  /[Aa][Dd][Mm][Ii][Ss]{2}[Ii][Oo][Nn]/;
+    const acAppPattern0 = /[Aa][Dd][Mm][Ii][Nn]/;
+    const acAppPattern1 = /[Aa][Dd][Mm][Ii][Ss]{2}[Ii][Oo][Nn]/;
     if (acAppPattern0.test(app.xapp) || acAppPattern1.test(app.xapp)) {
       this.router.navigate(['/ac']);
-    }  else {
+    } else {
       this.errorDialogService.displayError('No control available for ' + app.xapp + ' (yet)');
     }
   }
 
   onUndeployApp(app: XappControlRow): void {
     this.confirmDialogService.openConfirmDialog('Are you sure you want to undeploy App ' + app.xapp + '?')
-      .afterClosed().subscribe( (res: boolean) => {
+      .afterClosed().subscribe((res: boolean) => {
         if (res) {
           this.loadingDialogService.startLoading("Undeploying " + app.xapp);
-          this.appMgrSvc.undeployXapp(app.xapp)
+          this.appMgrSvc.undeployXapp(this.instanceKey, app.xapp)
             .pipe(
               finalize(() => this.loadingDialogService.stopLoading())
             )
             .subscribe(
-            httpResponse: HttpResponse<Object>) => {
-              // Answers 204/No content on success
-              this.notificationService.success('App undeployed successfully!');
-              this.dataSource.loadTable();
-            },
-            (her: HttpErrorResponse) => {
-              // the error field should have an ErrorTransport object
-              let msg = her.message;
-              if (her.error && her.error.message) {
-                msg = her.error.message;
-              }
-              this.notificationService.warn('App undeploy failed: ' + msg);
-            })
-          );
+              (httpResponse: HttpResponse<Object>) => {
+                // Answers 204/No content on success
+                this.notificationService.success('App undeployed successfully!');
+                this.dataSource.loadTable(this.instanceKey);
+              },
+              ((her: HttpErrorResponse) => {
+                // the error field should have an ErrorTransport object
+                let msg = her.message;
+                if (her.error && her.error.message) {
+                  msg = her.error.message;
+                }
+                this.notificationService.warn('App undeploy failed: ' + msg);
+              })
+            );
         }
       });
   }
index e97dc63..76c4216 100644 (file)
@@ -41,7 +41,8 @@ export class AppControlDataSource extends DataSource<XappControlRow> {
   public rowCount = 1; // hide footer during intial load
 
   private emptyInstances: XMXappInstance =
-    { ip: null,
+    {
+      ip: null,
       name: null,
       port: null,
       status: null,
@@ -55,18 +56,18 @@ export class AppControlDataSource extends DataSource<XappControlRow> {
     super();
   }
 
-  loadTable() {
+  loadTable(instanceKey: string) {
     this.loadingSubject.next(true);
-    this.appMgrSvc.getDeployed()
+    this.appMgrSvc.getDeployed(instanceKey)
       .pipe(
-        catchError( (her: HttpErrorResponse) => {
+        catchError((her: HttpErrorResponse) => {
           console.log('AppControlDataSource failed: ' + her.message);
           this.notificationService.error('Failed to get applications: ' + her.message);
           return of([]);
         }),
         finalize(() => this.loadingSubject.next(false))
       )
-      .subscribe( (xApps: XMDeployedApp[]) => {
+      .subscribe((xApps: XMDeployedApp[]) => {
         this.rowCount = xApps.length;
         const flattenedApps = this.flatten(xApps);
         this.appControlSubject.next(flattenedApps);
@@ -88,7 +89,7 @@ export class AppControlDataSource extends DataSource<XappControlRow> {
     this.loadingSubject.complete();
   }
 
-  private flatten(allxappdata: XMDeployedApp[]): XappControlRow[]  {
+  private flatten(allxappdata: XMDeployedApp[]): XappControlRow[] {
     const xAppInstances: XappControlRow[] = [];
     for (const xapp of allxappdata) {
       if (!xapp.instances) {
index 73aaf61..3f6e0e7 100644 (file)
 
 import { Component, Input, OnInit, ViewChild } from '@angular/core';
 import { MatSort } from '@angular/material/sort';
-import { CaasIngressDataSource } from './caas-ingress.datasource';
+import { Subscription } from 'rxjs';
 import { CaasIngressService } from '../services/caas-ingress/caas-ingress.service';
+import { InstanceSelectorService } from '../services/instance-selector/instance-selector.service';
 import { ConfirmDialogService } from '../services/ui/confirm-dialog.service';
 import { ErrorDialogService } from '../services/ui/error-dialog.service';
 import { LoadingDialogService } from '../services/ui/loading-dialog.service';
 import { NotificationService } from '../services/ui/notification.service';
+import { CaasIngressDataSource } from './caas-ingress.datasource';
 
 @Component({
   selector: 'rd-caas-ingress',
@@ -40,19 +42,29 @@ export class CaasIngressComponent implements OnInit {
   @Input() namespace: string;
 
   dataSource: CaasIngressDataSource;
-  displayedColumns: string[] = [ 'namespace', 'name', 'status', 'ip', 'containers', 'restartCount', 'creationTime' ];
+  displayedColumns: string[] = ['namespace', 'name', 'status', 'ip', 'containers', 'restartCount', 'creationTime'];
   @ViewChild(MatSort, { static: true }) sort: MatSort;
+  private instanceChange: Subscription;
 
   constructor(
     private caasIngressSvc: CaasIngressService,
     private confirmDialogService: ConfirmDialogService,
     private errorDialogService: ErrorDialogService,
     private loadingDialogService: LoadingDialogService,
+    public instanceSelectorService: InstanceSelectorService,
     private notificationService: NotificationService) { }
 
   ngOnInit() {
     this.dataSource = new CaasIngressDataSource(this.caasIngressSvc, this.sort, this.notificationService);
-    this.dataSource.loadTable(this.cluster, this.namespace);
+    this.instanceChange = this.instanceSelectorService.getSelectedInstancekey().subscribe((instanceKey: string) => {
+      if (instanceKey) {
+        this.dataSource.loadTable(instanceKey, this.cluster, this.namespace);
+      }
+    })
+  }
+
+  ngOnDestroy() {
+    this.instanceChange.unsubscribe();
   }
 
 }
index 0134bb6..637de03 100644 (file)
@@ -46,9 +46,9 @@ export class CaasIngressDataSource extends DataSource<V1Pod> {
     super();
   }
 
-  loadTable(cluster: string, namespace: string) {
+  loadTable(instanceKey: string, cluster: string, namespace: string) {
     this.loadingSubject.next(true);
-    this.caasIngressService.getPodList(cluster, namespace)
+    this.caasIngressService.getPodList(instanceKey, cluster, namespace)
       .pipe(
         catchError((her: HttpErrorResponse) => {
           console.log('CaasIngressDataSource failed: ' + her.message);
index 07e0e02..d1c1400 100644 (file)
  * ========================LICENSE_END===================================
  */
 import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
-import { Component, OnInit, ViewChild } from '@angular/core';
+import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
 import { MatDialog } from '@angular/material/dialog';
 import { MatSort } from '@angular/material/sort';
+import { Subscription } from 'rxjs';
 import { finalize } from 'rxjs/operators';
 import { XMDeployableApp } from '../interfaces/app-mgr.types';
 import { AppMgrService } from '../services/app-mgr/app-mgr.service';
-import { ErrorDialogService } from '../services/ui/error-dialog.service';
+import { InstanceSelectorService } from '../services/instance-selector/instance-selector.service';
 import { LoadingDialogService } from '../services/ui/loading-dialog.service';
+import { UiService } from '../services/ui/ui.service';
 import { AppConfigurationComponent } from './../app-configuration/app-configuration.component';
 import { ConfirmDialogService } from './../services/ui/confirm-dialog.service';
 import { NotificationService } from './../services/ui/notification.service';
 import { CatalogDataSource } from './catalog.datasource';
-import { UiService } from '../services/ui/ui.service';
 
 @Component({
   selector: 'rd-app-catalog',
   templateUrl: './catalog.component.html',
   styleUrls: ['./catalog.component.scss'],
 })
-export class CatalogComponent implements OnInit {
+export class CatalogComponent implements OnInit, OnDestroy {
 
   darkMode: boolean;
   panelClass: string = "";
   displayedColumns: string[] = ['name', 'version', 'action'];
   dataSource: CatalogDataSource;
+  private instanceChange: Subscription;
+  private instanceKey: string;
+
   @ViewChild(MatSort, { static: true }) sort: MatSort;
 
   constructor(
     private appMgrService: AppMgrService,
     private confirmDialogService: ConfirmDialogService,
     private dialog: MatDialog,
-    private errorDiaglogService: ErrorDialogService,
     private loadingDialogService: LoadingDialogService,
     private notificationService: NotificationService,
+    public instanceSelectorService: InstanceSelectorService,
     public ui: UiService) { }
 
   ngOnInit() {
     this.dataSource = new CatalogDataSource(this.appMgrService, this.sort, this.notificationService);
-    this.dataSource.loadTable();
     this.ui.darkModeState.subscribe((isDark) => {
       this.darkMode = isDark;
     });
+
+    this.instanceChange = this.instanceSelectorService.getSelectedInstancekey().subscribe((instanceKey: string) => {
+      if (instanceKey) {
+        this.instanceKey = instanceKey;
+        this.dataSource.loadTable(instanceKey);
+      }
+    })
+  }
+
+  ngOnDestroy() {
+    this.instanceChange.unsubscribe();
   }
 
   onConfigureApp(xapp: XMDeployableApp): void {
@@ -75,7 +89,11 @@ export class CatalogComponent implements OnInit {
       position: {
         top: '10%'
       },
-      data: xapp
+      data: {
+        xapp: xapp,
+        instanceKey: this.instanceKey
+      }
+
     })
   }
 
@@ -84,7 +102,7 @@ export class CatalogComponent implements OnInit {
       .afterClosed().subscribe((res: boolean) => {
         if (res) {
           this.loadingDialogService.startLoading('Deploying ' + app.name);
-          this.appMgrService.deployXapp(app.name)
+          this.appMgrService.deployXapp(this.instanceKey, app.name)
             .pipe(
               finalize(() => this.loadingDialogService.stopLoading())
             )
index f5f6e98..68362a7 100644 (file)
@@ -33,31 +33,29 @@ import { NotificationService } from '../services/ui/notification.service';
 export class CatalogDataSource extends DataSource<XMDeployableApp> {
 
   private catalogSubject = new BehaviorSubject<XMDeployableApp[]>([]);
-
   private loadingSubject = new BehaviorSubject<boolean>(false);
-
   public loading$ = this.loadingSubject.asObservable();
-
   public rowCount = 1; // hide footer during intial load
 
-  constructor(private appMgrSvc: AppMgrService,
+  constructor(
+    private appMgrSvc: AppMgrService,
     private sort: MatSort,
     private notificationService: NotificationService) {
     super();
   }
 
-  loadTable() {
+  loadTable(instanceKey: string) {
     this.loadingSubject.next(true);
-    this.appMgrSvc.getDeployable()
+    this.appMgrSvc.getDeployable(instanceKey)
       .pipe(
-        catchError( (her: HttpErrorResponse) => {
+        catchError((her: HttpErrorResponse) => {
           console.log('CatalogDataSource failed: ' + her.message);
           this.notificationService.error('Failed to get applications: ' + her.message);
           return of([]);
         }),
         finalize(() => this.loadingSubject.next(false))
       )
-      .subscribe( (xApps: XMDeployableApp[]) => {
+      .subscribe((xApps: XMDeployableApp[]) => {
         this.rowCount = xApps.length;
         this.catalogSubject.next(xApps);
       });
index 90dfd15..1c57005 100644 (file)
@@ -55,3 +55,8 @@ export interface EcompUser {
   active: boolean;
   [position: number]: EcompRole;
 }
+
+export interface RicInstance {
+  key: string;
+  name: string;
+}
\ No newline at end of file
index ad87121..58120b0 100644 (file)
@@ -18,9 +18,9 @@
  * ========================LICENSE_END===================================
  */
 import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
-import { Component, OnInit } from '@angular/core';
+import { Component, Inject, OnInit } from '@angular/core';
 import { FormControl, FormGroup, Validators } from '@angular/forms';
-import { MatDialogRef } from '@angular/material/dialog';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
 import { Observable } from 'rxjs';
 import { finalize } from 'rxjs/operators';
 import { E2SetupRequest, RanDialogFormData } from '../interfaces/e2-mgr.types';
@@ -45,7 +45,8 @@ export class RanControlConnectDialogComponent implements OnInit {
     private service: E2ManagerService,
     private errorService: ErrorDialogService,
     private loadingDialogService: LoadingDialogService,
-    private notifService: NotificationService) {
+    private notifService: NotificationService,
+    @Inject(MAT_DIALOG_DATA) private data) {
     // opens with empty fields; accepts no data to display
   }
 
@@ -80,9 +81,9 @@ export class RanControlConnectDialogComponent implements OnInit {
     this.loadingDialogService.startLoading('Setting up connection');
     let observable: Observable<HttpResponse<Object>>;
     if (ranFormValue.ranType === 'endc') {
-      observable = this.service.endcSetup(setupRequest);
+      observable = this.service.endcSetup(this.data.instanceKey, setupRequest);
     } else {
-      observable = this.service.x2Setup(setupRequest);
+      observable = this.service.x2Setup(this.data.instanceKey, setupRequest);
     }
     observable
       .pipe(
index b5aba66..fa2f7f0 100644 (file)
 import { HttpErrorResponse } from '@angular/common/http';
 import { Component, OnInit } from '@angular/core';
 import { MatDialog } from '@angular/material/dialog';
+import { Subscription } from 'rxjs';
 import { finalize } from 'rxjs/operators';
 import { E2ManagerService } from '../services/e2-mgr/e2-mgr.service';
+import { InstanceSelectorService } from '../services/instance-selector/instance-selector.service';
 import { ConfirmDialogService } from '../services/ui/confirm-dialog.service';
 import { ErrorDialogService } from '../services/ui/error-dialog.service';
 import { LoadingDialogService } from '../services/ui/loading-dialog.service';
 import { NotificationService } from '../services/ui/notification.service';
+import { UiService } from '../services/ui/ui.service';
 import { RanControlConnectDialogComponent } from './ran-connection-dialog.component';
 import { RANControlDataSource } from './ran-control.datasource';
-import { UiService } from '../services/ui/ui.service';
 
 @Component({
   selector: 'rd-ran-control',
@@ -41,21 +43,35 @@ export class RanControlComponent implements OnInit {
   panelClass: string = "";
   displayedColumns: string[] = ['nbId', 'nodeType', 'ranName', 'ranIp', 'ranPort', 'connectionStatus'];
   dataSource: RANControlDataSource;
+  private instanceChange: Subscription;
+  private instanceKey: string;
 
   constructor(private e2MgrSvc: E2ManagerService,
     private errorDialogService: ErrorDialogService,
     private confirmDialogService: ConfirmDialogService,
     private notificationService: NotificationService,
     private loadingDialogService: LoadingDialogService,
+    public instanceSelectorService: InstanceSelectorService,
     public dialog: MatDialog,
     public ui: UiService) { }
 
   ngOnInit() {
     this.dataSource = new RANControlDataSource(this.e2MgrSvc, this.notificationService);
-    this.dataSource.loadTable();
+
     this.ui.darkModeState.subscribe((isDark) => {
       this.darkMode = isDark;
     });
+
+    this.instanceChange = this.instanceSelectorService.getSelectedInstancekey().subscribe((instanceKey: string) => {
+      if (instanceKey) {
+        this.instanceKey = instanceKey;
+        this.dataSource.loadTable(instanceKey);
+      }
+    })
+  }
+
+  ngOnDestroy() {
+    this.instanceChange.unsubscribe();
   }
 
   setupRANConnection() {
@@ -66,40 +82,43 @@ export class RanControlComponent implements OnInit {
     }
     const dialogRef = this.dialog.open(RanControlConnectDialogComponent, {
       panelClass: this.panelClass,
-      width: '450px'
+      width: '450px',
+      data: {
+        instanceKey: this.instanceKey
+      }
     });
     dialogRef.afterClosed()
       .subscribe((result: boolean) => {
-      if (result) {
-        this.dataSource.loadTable();
-      }
-    });
+        if (result) {
+          this.dataSource.loadTable(this.instanceKey);
+        }
+      });
   }
 
   disconnectAllRANConnections() {
     const aboutError = 'Disconnect all RAN Connections Failed: ';
     this.confirmDialogService.openConfirmDialog('Are you sure you want to disconnect all RAN connections?')
-      .afterClosed().subscribe( (res: boolean) => {
+      .afterClosed().subscribe((res: boolean) => {
         if (res) {
           this.loadingDialogService.startLoading("Disconnecting");
-          this.e2MgrSvc.nodebPut()
+          this.e2MgrSvc.nodebPut(this.instanceKey)
             .pipe(
               finalize(() => this.loadingDialogService.stopLoading())
             )
             .subscribe(
-            ( body: any ) => {
-              this.notificationService.success('Disconnect succeeded!');
-              this.dataSource.loadTable();
-            },
-            (her: HttpErrorResponse) => {
-              // the error field should have an ErrorTransport object
-              let msg = her.message;
-              if (her.error && her.error.message) {
-                msg = her.error.message;
+              (body: any) => {
+                this.notificationService.success('Disconnect succeeded!');
+                this.dataSource.loadTable(this.instanceKey);
+              },
+              (her: HttpErrorResponse) => {
+                // the error field should have an ErrorTransport object
+                let msg = her.message;
+                if (her.error && her.error.message) {
+                  msg = her.error.message;
+                }
+                this.errorDialogService.displayError('Disconnect failed: ' + msg);
               }
-              this.errorDialogService.displayError('Disconnect failed: ' + msg);
-            }
-          );
+            );
         }
       });
   }
index b23a3cd..503f459 100644 (file)
@@ -43,9 +43,9 @@ export class RANControlDataSource extends DataSource<E2RanDetails> {
     super();
   }
 
-  loadTable() {
+  loadTable(instanceKey: string) {
     this.loadingSubject.next(true);
-    this.e2MgrSvcservice.getRan()
+    this.e2MgrSvcservice.getRan(instanceKey)
       .pipe(
         catchError( (her: HttpErrorResponse) => {
           console.log('RANControlDataSource failed: ' + her.message);
index 7146b37..85cbbcd 100644 (file)
            viewBox="31.5 30 49.9 32">
         <rect id="Rectangle_9" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
               class="hamburger__icon__fill" data-name="Rectangle 9" rx="2"
-              transform="translate(31.5 58)"/>
+              transform="translate(31.5 58)" />
         <rect id="Rectangle_10" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
               class="hamburger__icon__fill" data-name="Rectangle 10" rx="2"
-              transform="translate(31.5 44)"/>
+              transform="translate(31.5 44)" />
         <rect id="Rectangle_11" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
               class="hamburger__icon__fill" data-name="Rectangle 11" rx="2"
-              transform="translate(31.5 30)"/>
+              transform="translate(31.5 30)" />
       </svg>
-      <img src="../../assets/oran-logo.png" width="180%" height="100%" style="position: relative; z-index: 50"/>
+      <img src="../../assets/oran-logo.png" width="180%" height="100%" style="position: relative; z-index: 50" />
       <svg class="logo__icon" viewBox="150.3 22.2 313.7 42.8">
         <text [ngClass]="{'logo__text-dark': darkModeActive}" class="logo__text" fill="#432c85"
               font-size="30" font-weight="600"
 
     </div>
 
-    <h3 class="date__text"></h3>
+    <div>
+      <mat-label class="selector-label">Select RIC Instance</mat-label>
+      <mat-form-field>
+        <mat-select (selectionChange)="changeInstance($event.value)" [(value)]="selectedInstanceKey">
+          <mat-option *ngFor="let instance of instanceArray" [value]="instance.key">
+            {{instance.name}}
+          </mat-option>
+        </mat-select>
+      </mat-form-field>
+    </div>
 
     <div class="mode-toggle__container">
       <span class="mode-toggle__text">Light</span>
       <label class="toggle-button__container">
-        <input (click)="modeToggleSwitch()" type="checkbox" class="mode-toggle__input"/>
+        <input (click)="modeToggleSwitch()" type="checkbox" class="mode-toggle__input" />
         <span [ngClass]="{'mode-toggle__bg-checked': darkModeActive}" class="mode-toggle__bg"></span>
         <span [ngClass]="{'mode-toggle__circle-checked': darkModeActive}" class="mode-toggle__circle"></span>
       </label>
index 4a49487..7245077 100644 (file)
@@ -363,3 +363,7 @@ mat-drawer-content {
     width: 100%;
   }
 }
+
+.selector-label {
+    margin-right: 20px;
+}
\ No newline at end of file
index 1673280..16a1849 100644 (file)
  * ========================LICENSE_END===================================
  */
 import { Component, OnInit } from '@angular/core';
+import { finalize } from 'rxjs/operators';
+import { RicInstance } from './interfaces/dashboard.types';
+import { InstanceSelectorService } from './services/instance-selector/instance-selector.service';
+import { LoadingDialogService } from './services/ui/loading-dialog.service';
 import { UiService } from './services/ui/ui.service';
 
 @Component({
@@ -28,14 +32,28 @@ import { UiService } from './services/ui/ui.service';
 export class RdComponent implements OnInit {
   showMenu = false;
   darkModeActive: boolean;
+  private instanceArray: RicInstance[];
+  private selectedInstanceKey: string;
 
-  constructor(public ui: UiService) {
+  constructor(
+    public ui: UiService,
+    private instanceSelectorService: InstanceSelectorService,
+    private loadingDialogService: LoadingDialogService) {
   }
 
   ngOnInit() {
     this.ui.darkModeState.subscribe((value) => {
       this.darkModeActive = value;
     });
+    this.loadingDialogService.startLoading('Loading RIC instances');
+    this.instanceSelectorService.getInstanceArray()
+      .pipe(
+        finalize(() => this.loadingDialogService.stopLoading())
+      )
+      .subscribe((instanceArray: RicInstance[]) => {
+        this.instanceArray = instanceArray;
+        this.selectedInstanceKey = instanceArray[0].key;
+      })
   }
 
   toggleMenu() {
@@ -46,4 +64,8 @@ export class RdComponent implements OnInit {
     this.ui.darkModeState.next(!this.darkModeActive);
   }
 
+  changeInstance(selectedInstancekey: string) {
+    this.instanceSelectorService.updateSelectedInstance(selectedInstancekey);
+  }
+
 }
index 440184f..e9944f1 100644 (file)
  * limitations under the License.
  * ========================LICENSE_END===================================
  */
-import { BrowserModule } from '@angular/platform-browser';
+import { HttpClientModule } from '@angular/common/http';
+import { NgModule } from '@angular/core';
+import { FlexLayoutModule } from '@angular/flex-layout';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { MatButtonModule } from '@angular/material/button';
 import { MatButtonToggleModule } from '@angular/material/button-toggle';
 import { MatCardModule } from '@angular/material/card';
@@ -31,6 +34,7 @@ import { MatInputModule } from '@angular/material/input';
 import { MatListModule } from '@angular/material/list';
 import { MatPaginatorModule } from '@angular/material/paginator';
 import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { MatRadioModule } from '@angular/material/radio';
 import { MatSelectModule } from '@angular/material/select';
 import { MatSidenavModule } from '@angular/material/sidenav';
 import { MatSlideToggleModule } from '@angular/material/slide-toggle';
@@ -39,47 +43,48 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
 import { MatSortModule } from '@angular/material/sort';
 import { MatTableModule } from '@angular/material/table';
 import { MatTabsModule } from '@angular/material/tabs';
-import { BrowserAnimationsModule} from '@angular/platform-browser/animations';
-import { HttpClientModule } from '@angular/common/http';
-import { NgModule } from '@angular/core';
-import { MatRadioModule } from '@angular/material/radio';
 import { MatTooltipModule } from '@angular/material/tooltip';
-import { ChartsModule } from 'ng2-charts';
+import { BrowserModule } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { MDBBootstrapModule } from 'angular-bootstrap-md';
-import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MaterialDesignFrameworkModule } from 'angular6-json-schema-form';
+import { ChartsModule } from 'ng2-charts';
 import { ToastrModule } from 'ngx-toastr';
+
 import { AcXappComponent } from './ac-xapp/ac-xapp.component';
-import { AddDashboardUserDialogComponent } from './user/add-dashboard-user-dialog/add-dashboard-user-dialog.component';
 import { AppConfigurationComponent } from './app-configuration/app-configuration.component';
 import { AppControlComponent } from './app-control/app-control.component';
-import { AppMgrService } from './services/app-mgr/app-mgr.service';
 import { CaasIngressComponent } from './caas-ingress/caas-ingress.component';
-import { CatalogCardComponent } from './ui/catalog-card/catalog-card.component';
 import { CatalogComponent } from './catalog/catalog.component';
-import { ConfirmDialogComponent } from './ui/confirm-dialog/confirm-dialog.component';
-import { ControlCardComponent } from './ui/control-card/control-card.component';
 import { ControlComponent } from './control/control.component';
-import { DashboardService } from './services/dashboard/dashboard.service';
-import { E2ManagerService } from './services/e2-mgr/e2-mgr.service';
-import { EditDashboardUserDialogComponent } from './user/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component';
-import { ErrorDialogComponent } from './ui/error-dialog/error-dialog.component';
-import { ErrorDialogService } from './services/ui/error-dialog.service';
-import { FlexLayoutModule } from '@angular/flex-layout';
+import { CommonService } from './services/common/common.service';
 import { FooterComponent } from './footer/footer.component';
-import { LoadingDialogComponent } from './ui/loading-dialog/loading-dialog.component';
 import { MainComponent } from './main/main.component';
-import { MaterialDesignFrameworkModule } from 'angular6-json-schema-form';
+import { SidenavListComponent } from './navigation/sidenav-list/sidenav-list.component';
 import { PlatformComponent } from './platform/platform.component';
-import { RanControlComponent } from './ran-control/ran-control.component';
 import { RanControlConnectDialogComponent } from './ran-control/ran-connection-dialog.component';
-import { RdComponent } from './rd.component';
+import { RanControlComponent } from './ran-control/ran-control.component';
 import { RdRoutingModule } from './rd-routing.module';
-import { SidenavListComponent } from './navigation/sidenav-list/sidenav-list.component';
-import { StatCardComponent } from './ui/stat-card/stat-card.component';
-import { StatsComponent } from './stats/stats.component';
+import { RdComponent } from './rd.component';
+import { AppMgrService } from './services/app-mgr/app-mgr.service';
+import { DashboardService } from './services/dashboard/dashboard.service';
+import { E2ManagerService } from './services/e2-mgr/e2-mgr.service';
+import { InstanceSelectorService } from './services/instance-selector/instance-selector.service';
+import { ErrorDialogService } from './services/ui/error-dialog.service';
 import { UiService } from './services/ui/ui.service';
+import { StatsComponent } from './stats/stats.component';
+import { CatalogCardComponent } from './ui/catalog-card/catalog-card.component';
+import { ConfirmDialogComponent } from './ui/confirm-dialog/confirm-dialog.component';
+import { ControlCardComponent } from './ui/control-card/control-card.component';
+import { ErrorDialogComponent } from './ui/error-dialog/error-dialog.component';
+import { LoadingDialogComponent } from './ui/loading-dialog/loading-dialog.component';
+import { StatCardComponent } from './ui/stat-card/stat-card.component';
+import { AddDashboardUserDialogComponent } from './user/add-dashboard-user-dialog/add-dashboard-user-dialog.component';
+import { EditDashboardUserDialogComponent } from './user/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component';
 import { UserComponent } from './user/user.component';
 
+
+
 @NgModule({
   declarations: [
     AcXappComponent,
@@ -172,9 +177,11 @@ import { UserComponent } from './user/user.component';
   ],
   providers: [
     AppMgrService,
+    CommonService,
     DashboardService,
     E2ManagerService,
     ErrorDialogService,
+    InstanceSelectorService,
     UiService
   ],
   bootstrap: [RdComponent]
index d25e9db..6e67b32 100644 (file)
  * ========================LICENSE_END===================================
  */
 
- import { Injectable } from '@angular/core';
- import { HttpClient } from '@angular/common/http';
- import { Observable } from 'rxjs';
- import { map } from 'rxjs/operators';
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
 import { ACAdmissionIntervalControl, ACAdmissionIntervalControlAck } from '../../interfaces/ac-xapp.types';
 import { DashboardSuccessTransport } from '../../interfaces/dashboard.types';
+import { CommonService } from '../common/common.service';
 
 /**
  * Services for calling the Dashboard's A1 endpoints to get/put AC policies.
@@ -33,19 +34,13 @@ import { DashboardSuccessTransport } from '../../interfaces/dashboard.types';
 })
 export class ACXappService {
 
-  private basePath = 'api/a1-p';
+  private component = 'a1-p';
   private policyPath = 'policies';
   private acPolicyName = 'admission_control_policy';
 
-  private buildPath(...args: any[]) {
-    let result = this.basePath;
-    args.forEach(part => {
-      result = result + '/' + part;
-    });
-    return result;
-  }
-
-  constructor(private httpClient: HttpClient) {
+  constructor(
+    private httpClient: HttpClient,
+    private commonSvc: CommonService) {
     // injects to variable httpClient
   }
 
@@ -54,7 +49,7 @@ export class ACXappService {
    * @returns Observable that should yield a String
    */
   getVersion(): Observable<string> {
-    const url = this.buildPath('version');
+    const url = 'api/a1-p/version'
     return this.httpClient.get<DashboardSuccessTransport>(url).pipe(
       // Extract the string here
       map(res => res['data'])
@@ -65,8 +60,8 @@ export class ACXappService {
    * Gets admission control policy.
    * @returns Observable that should yield an ACAdmissionIntervalControl
    */
-  getPolicy(): Observable<ACAdmissionIntervalControl> {
-    const url = this.buildPath(this.policyPath, this.acPolicyName);
+  getPolicy(instanceKey: string): Observable<ACAdmissionIntervalControl> {
+    const url = this.commonSvc.buildPath(instanceKey, this.component, this.policyPath, this.acPolicyName);
     return this.httpClient.get<ACAdmissionIntervalControl>(url);
   }
 
@@ -75,8 +70,8 @@ export class ACXappService {
    * @param policy an instance of ACAdmissionIntervalControl
    * @returns Observable that should yield a response code, no data
    */
-  putPolicy(policy: ACAdmissionIntervalControl): Observable<any> {
-    const url = this.buildPath(this.policyPath, this.acPolicyName);
+  putPolicy(instanceKey: string, policy: ACAdmissionIntervalControl): Observable<any> {
+    const url = this.commonSvc.buildPath(instanceKey, this.component, this.policyPath, this.acPolicyName);
     return this.httpClient.put<ACAdmissionIntervalControlAck>(url, policy, { observe: 'response' });
   }
 
index afe7a65..315139e 100644 (file)
  * limitations under the License.
  * ========================LICENSE_END===================================
  */
-import { Injectable } from '@angular/core';
 import { HttpClient, HttpResponse } from '@angular/common/http';
+import { Injectable } from '@angular/core';
 import { Observable } from 'rxjs';
-import { XMXappInfo, XMDeployableApp, XMDeployedApp } from '../../interfaces/app-mgr.types';
+import { XMDeployableApp, XMDeployedApp, XMXappInfo } from '../../interfaces/app-mgr.types';
+import { CommonService } from '../common/common.service';
 
 @Injectable()
 export class AppMgrService {
 
-  constructor(private httpClient: HttpClient) {
-    // injects to variable httpClient
-  }
+  private component = 'appmgr';
 
-  private basePath = 'api/appmgr';
+  constructor(
+    private httpClient: HttpClient,
+    private commonSvc: CommonService) {
+  }
 
-  getDeployable(): Observable<XMDeployableApp[]> {
-    return this.httpClient.get<XMDeployableApp[]>(this.basePath + '/xapps/list');
+  getDeployable(instanceKey: string): Observable<XMDeployableApp[]> {
+    const url = this.commonSvc.buildPath(instanceKey, this.component, 'xapps', 'list');
+    return this.httpClient.get<XMDeployableApp[]>(url);
   }
 
-  getDeployed(): Observable<XMDeployedApp[]> {
-    return this.httpClient.get<XMDeployedApp[]>(this.basePath + '/xapps');
+  getDeployed(instanceKey: string): Observable<XMDeployedApp[]> {
+    const url = this.commonSvc.buildPath(instanceKey, this.component, 'xapps');
+    return this.httpClient.get<XMDeployedApp[]>(url);
   }
 
-  deployXapp(name: string): Observable<HttpResponse<Object>> {
+  deployXapp(instanceKey: string, name: string): Observable<HttpResponse<Object>> {
     const xappInfo: XMXappInfo = { name: name };
-    return this.httpClient.post((this.basePath + '/xapps'), xappInfo, { observe: 'response' });
+    const url = this.commonSvc.buildPath(instanceKey, this.component, 'xapps');
+    return this.httpClient.post(url, xappInfo, { observe: 'response' });
   }
 
-  undeployXapp(name: string): Observable<HttpResponse<Object>> {
-    return this.httpClient.delete((this.basePath + '/xapps'+ '/' + name), { observe: 'response' });
+  undeployXapp(instanceKey: string, name: string): Observable<HttpResponse<Object>> {
+    const url = this.commonSvc.buildPath(instanceKey, this.component, 'xapps', name);
+    return this.httpClient.delete(url, { observe: 'response' });
   }
 
-  getConfig(): Observable<any[]>{
+  getConfig(instanceKey: string): Observable<any[]> {
+    // For demo purpose, pull example config from local
     return this.httpClient.get<any[]>("/assets/mockdata/config.json");
-    //return this.httpClient.get<any[]>((this.basePath  + '/config'));
+    // Once Xapp manager contains layout, should call backend to get xapp config 
+    //const url = this.commonSvc.buildPath(instanceKey, this.component, 'config');
+    //return this.httpClient.get<any[]>(url);
   }
 
-  putConfig(config: any): Observable<HttpResponse<Object>> {
-    return this.httpClient.put((this.basePath + '/config' ), config, { observe: 'response' });
+  putConfig(instanceKey: string, config: any): Observable<HttpResponse<Object>> {
+    const url = this.commonSvc.buildPath(instanceKey, this.component, 'config');
+    return this.httpClient.put(url, config, { observe: 'response' });
   }
 
-
 }
index 359a08f..42cb928 100644 (file)
  * ========================LICENSE_END===================================
  */
 
-import { Injectable } from '@angular/core';
 import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
 import { V1PodList } from '@kubernetes/client-node';
 import { Observable } from 'rxjs';
+import { CommonService } from '../common/common.service';
 
 /**
 * Services for calling the Dashboard's caas-ingress endpoints to get Kubernetes details.
@@ -31,18 +32,12 @@ import { Observable } from 'rxjs';
 })
 export class CaasIngressService {
 
-  private basePath = 'api/caas-ingress';
+  private component = 'caas-ingress';
   private podsPath = 'pods';
 
-  private buildPath(...args: any[]) {
-    let result = this.basePath;
-    args.forEach(part => {
-      result = result + '/' + part;
-    });
-    return result;
-  }
-
-  constructor(private httpClient: HttpClient) {
+  constructor(
+    private httpClient: HttpClient,
+    private commonSvc: CommonService) {
     // injects to variable httpClient
   }
 
@@ -50,8 +45,8 @@ export class CaasIngressService {
    * Gets list of pods
    * @returns Observable that should yield a V1PodList
    */
-  getPodList(cluster: string, namespace: string): Observable<V1PodList> {
-    const url = this.buildPath('pods', 'cluster', cluster, 'namespace', namespace);
+  getPodList(instanceKey: string, cluster: string, namespace: string): Observable<V1PodList> {
+    const url = this.commonSvc.buildPath(instanceKey, this.component, 'pods', 'cluster', cluster, 'namespace', namespace);
     return this.httpClient.get<V1PodList>(url);
   }
 
diff --git a/webapp-frontend/src/app/services/common/common.service.spec.ts b/webapp-frontend/src/app/services/common/common.service.spec.ts
new file mode 100644 (file)
index 0000000..54eaeb5
--- /dev/null
@@ -0,0 +1,32 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+import { TestBed } from '@angular/core/testing';
+
+import { CommonService } from './common.service';
+
+describe('CommonService', () => {
+  beforeEach(() => TestBed.configureTestingModule({}));
+
+  it('should be created', () => {
+    const service: CommonService = TestBed.get(CommonService);
+    expect(service).toBeTruthy();
+  });
+});
diff --git a/webapp-frontend/src/app/services/common/common.service.ts b/webapp-frontend/src/app/services/common/common.service.ts
new file mode 100644 (file)
index 0000000..82d2cf2
--- /dev/null
@@ -0,0 +1,38 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+import { Injectable } from '@angular/core';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class CommonService {
+
+  constructor() { }
+
+  buildPath(instanceKey: string, component: string, ...args: any[]) {
+    let result = 'api/' + component + '/ric/' + instanceKey;
+    args.forEach(part => {
+      result = result + '/' + part;
+    });
+    return result;
+  }
+
+}
index b3388cf..dc088a2 100644 (file)
  * limitations under the License.
  * ========================LICENSE_END===================================
  */
-import { Injectable } from '@angular/core';
 import { HttpClient, HttpResponse } from '@angular/common/http';
+import { Injectable } from '@angular/core';
 import { Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
-import { E2RanDetails, E2SetupRequest } from '../../interfaces/e2-mgr.types';
 import { DashboardSuccessTransport } from '../../interfaces/dashboard.types';
+import { E2RanDetails, E2SetupRequest } from '../../interfaces/e2-mgr.types';
+import { CommonService } from '../common/common.service';
 
 @Injectable({
   providedIn: 'root'
@@ -30,9 +31,11 @@ import { DashboardSuccessTransport } from '../../interfaces/dashboard.types';
 
 export class E2ManagerService {
 
-  private basePath = 'api/e2mgr/nodeb/';
+  private component = 'e2mgr';
 
-  constructor(private httpClient: HttpClient) {
+  constructor(
+    private httpClient: HttpClient,
+    private commonSvc: CommonService) {
     // injects to variable httpClient
   }
 
@@ -40,8 +43,8 @@ export class E2ManagerService {
    * Gets E2 client version details
    * @returns Observable that should yield a String
    */
-  getVersion(): Observable<string> {
-    const url = this.basePath + 'version';
+  getVersion(instanceKey: string): Observable<string> {
+    const url = 'api/e2mgr/version';
     return this.httpClient.get<DashboardSuccessTransport>(url).pipe(
       // Extract the string here
       map(res => res['data'])
@@ -52,32 +55,36 @@ export class E2ManagerService {
    * Gets RAN details
    * @returns Observable that should yield an array of objects
    */
-  getRan(): Observable<Array<E2RanDetails>> {
-    return this.httpClient.get<Array<E2RanDetails>>(this.basePath + 'ran');
+  getRan(instanceKey: string): Observable<Array<E2RanDetails>> {
+    const url = this.commonSvc.buildPath(instanceKey, this.component, 'nodeb', 'ran');
+    return this.httpClient.get<Array<E2RanDetails>>(url);
   }
 
   /**
    * Sends a request to setup an ENDC/gNodeB connection
    * @returns Observable. On success there is no data, only a code.
    */
-  endcSetup(req: E2SetupRequest): Observable<HttpResponse<Object>> {
-    return this.httpClient.post(this.basePath + 'endc-setup', req, { observe: 'response' });
+  endcSetup(instanceKey: string, req: E2SetupRequest): Observable<HttpResponse<Object>> {
+    const url = this.commonSvc.buildPath(instanceKey, this.component, 'nodeb', 'endc-setup');
+    return this.httpClient.post(url, req, { observe: 'response' });
   }
 
   /**
    * Sends a request to setup an X2/eNodeB connection
    * @returns Observable. On success there is no data, only a code.
    */
-  x2Setup(req: E2SetupRequest): Observable<HttpResponse<Object>> {
-    return this.httpClient.post(this.basePath + 'x2-setup', req, { observe: 'response' });
+  x2Setup(instanceKey: string, req: E2SetupRequest): Observable<HttpResponse<Object>> {
+    const url = this.commonSvc.buildPath(instanceKey, this.component, 'nodeb', 'x2-setup');
+    return this.httpClient.post(url, req, { observe: 'response' });
   }
 
   /**
    * Sends a request to drop all RAN connections
    * @returns Observable with body.
    */
-  nodebPut(): Observable<any> {
-    return this.httpClient.put((this.basePath + 'shutdown'), { observe: 'body' });
+  nodebPut(instanceKey: string): Observable<any> {
+    const url = this.commonSvc.buildPath(instanceKey, this.component, 'nodeb', 'shutdown');
+    return this.httpClient.put(url, { observe: 'body' });
   }
 
 }
diff --git a/webapp-frontend/src/app/services/instance-selector/instance-selector.service.spec.ts b/webapp-frontend/src/app/services/instance-selector/instance-selector.service.spec.ts
new file mode 100644 (file)
index 0000000..f00b02c
--- /dev/null
@@ -0,0 +1,32 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+import { TestBed } from '@angular/core/testing';
+
+import { InstanceSelectorService } from './instance-selector.service';
+
+describe('InstanceSelectorService', () => {
+  beforeEach(() => TestBed.configureTestingModule({}));
+
+  it('should be created', () => {
+    const service: InstanceSelectorService = TestBed.get(InstanceSelectorService);
+    expect(service).toBeTruthy();
+  });
+});
diff --git a/webapp-frontend/src/app/services/instance-selector/instance-selector.service.ts b/webapp-frontend/src/app/services/instance-selector/instance-selector.service.ts
new file mode 100644 (file)
index 0000000..202aa66
--- /dev/null
@@ -0,0 +1,67 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { shareReplay, tap } from 'rxjs/operators';
+import { RicInstance } from '../../interfaces/dashboard.types';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class InstanceSelectorService {
+  private selectedInstanceKey: BehaviorSubject<string> = new BehaviorSubject<string>('');
+  private instanceArray: Observable<RicInstance[]>;
+  private basePath = 'api/admin/instance';
+
+  constructor(private httpClient: HttpClient) { }
+
+  getInstanceArray(): Observable<RicInstance[]> {
+    if (this.instanceArray) {
+      return this.instanceArray;
+    }
+    return this.instanceArray = this.httpClient.get<RicInstance[]>(this.basePath)
+      .pipe(
+        tap(ricInstanceArray => {
+          this.initselectedInstanceKey(ricInstanceArray[0].key);
+        }),
+        shareReplay(1)
+      );
+  }
+
+  private initselectedInstanceKey(instanceKey: string) {
+    if (!this.selectedInstanceKey.value) {
+      this.selectedInstanceKey.next(instanceKey)
+    }
+  }
+
+  // This method may return the BehaviorSubject with empty string
+  // Afther subscribe that BehaviorSubject
+  // Please make sure this BehaviorSubject has non empty value
+  getSelectedInstancekey(): BehaviorSubject<string> {
+    return this.selectedInstanceKey;
+  }
+
+  updateSelectedInstance(instanceKey: string) {
+    this.selectedInstanceKey.next(instanceKey)
+  }
+
+}