From: Nicolas Hu Date: Wed, 27 Nov 2019 17:40:27 +0000 (-0500) Subject: support-multiple-ric-instances X-Git-Tag: 2.0.0~11 X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=commitdiff_plain;h=b7d7982bbfbf3a1333feb8b811f99497752d3722;p=portal%2Fric-dashboard.git support-multiple-ric-instances 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 --- diff --git a/docs/release-notes.rst b/docs/release-notes.rst index ddcd11ac..bb43e4dc 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -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 -------------------------- diff --git a/webapp-frontend/src/app/ac-xapp/ac-xapp.component.ts b/webapp-frontend/src/app/ac-xapp/ac-xapp.component.ts index 1e0df3b5..f85f18e7 100644 --- a/webapp-frontend/src/app/ac-xapp/ac-xapp.component.ts +++ b/webapp-frontend/src/app/ac-xapp/ac-xapp.component.ts @@ -18,13 +18,15 @@ * ========================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!'); diff --git a/webapp-frontend/src/app/app-configuration/app-configuration.component.html b/webapp-frontend/src/app/app-configuration/app-configuration.component.html index 28d6eac6..755e4e6d 100644 --- a/webapp-frontend/src/app/app-configuration/app-configuration.component.html +++ b/webapp-frontend/src/app/app-configuration/app-configuration.component.html @@ -19,14 +19,14 @@ -->
- Loading {{data.name}} Configuration + Loading {{data.xapp.name}} Configuration
- {{data.name}} Configuration + {{data.xapp.name}} Configuration
diff --git a/webapp-frontend/src/app/app-configuration/app-configuration.component.ts b/webapp-frontend/src/app/app-configuration/app-configuration.component.ts index 3c7a95c5..3e539aa8 100644 --- a/webapp-frontend/src/app/app-configuration/app-configuration.component.ts +++ b/webapp-frontend/src/app/app-configuration/app-configuration.component.ts @@ -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(); diff --git a/webapp-frontend/src/app/app-control/app-control.component.ts b/webapp-frontend/src/app/app-control/app-control.component.ts index c97a1950..35770dc8 100644 --- a/webapp-frontend/src/app/app-control/app-control.component.ts +++ b/webapp-frontend/src/app/app-control/app-control.component.ts @@ -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) => { - // 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) => { + // 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); + }) + ); } }); } diff --git a/webapp-frontend/src/app/app-control/app-control.datasource.ts b/webapp-frontend/src/app/app-control/app-control.datasource.ts index e97dc637..76c42168 100644 --- a/webapp-frontend/src/app/app-control/app-control.datasource.ts +++ b/webapp-frontend/src/app/app-control/app-control.datasource.ts @@ -41,7 +41,8 @@ export class AppControlDataSource extends DataSource { 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 { 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 { this.loadingSubject.complete(); } - private flatten(allxappdata: XMDeployedApp[]): XappControlRow[] { + private flatten(allxappdata: XMDeployedApp[]): XappControlRow[] { const xAppInstances: XappControlRow[] = []; for (const xapp of allxappdata) { if (!xapp.instances) { diff --git a/webapp-frontend/src/app/caas-ingress/caas-ingress.component.ts b/webapp-frontend/src/app/caas-ingress/caas-ingress.component.ts index 73aaf61a..3f6e0e76 100644 --- a/webapp-frontend/src/app/caas-ingress/caas-ingress.component.ts +++ b/webapp-frontend/src/app/caas-ingress/caas-ingress.component.ts @@ -20,12 +20,14 @@ 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(); } } diff --git a/webapp-frontend/src/app/caas-ingress/caas-ingress.datasource.ts b/webapp-frontend/src/app/caas-ingress/caas-ingress.datasource.ts index 0134bb64..637de03e 100644 --- a/webapp-frontend/src/app/caas-ingress/caas-ingress.datasource.ts +++ b/webapp-frontend/src/app/caas-ingress/caas-ingress.datasource.ts @@ -46,9 +46,9 @@ export class CaasIngressDataSource extends DataSource { 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); diff --git a/webapp-frontend/src/app/catalog/catalog.component.ts b/webapp-frontend/src/app/catalog/catalog.component.ts index 07e0e020..d1c14000 100644 --- a/webapp-frontend/src/app/catalog/catalog.component.ts +++ b/webapp-frontend/src/app/catalog/catalog.component.ts @@ -18,48 +18,62 @@ * ========================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()) ) diff --git a/webapp-frontend/src/app/catalog/catalog.datasource.ts b/webapp-frontend/src/app/catalog/catalog.datasource.ts index f5f6e986..68362a7c 100644 --- a/webapp-frontend/src/app/catalog/catalog.datasource.ts +++ b/webapp-frontend/src/app/catalog/catalog.datasource.ts @@ -33,31 +33,29 @@ import { NotificationService } from '../services/ui/notification.service'; export class CatalogDataSource extends DataSource { private catalogSubject = new BehaviorSubject([]); - private loadingSubject = new BehaviorSubject(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); }); diff --git a/webapp-frontend/src/app/interfaces/dashboard.types.ts b/webapp-frontend/src/app/interfaces/dashboard.types.ts index 90dfd150..1c570057 100644 --- a/webapp-frontend/src/app/interfaces/dashboard.types.ts +++ b/webapp-frontend/src/app/interfaces/dashboard.types.ts @@ -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 diff --git a/webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts b/webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts index ad871216..58120b07 100644 --- a/webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts +++ b/webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts @@ -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>; 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( diff --git a/webapp-frontend/src/app/ran-control/ran-control.component.ts b/webapp-frontend/src/app/ran-control/ran-control.component.ts index b5aba669..fa2f7f03 100644 --- a/webapp-frontend/src/app/ran-control/ran-control.component.ts +++ b/webapp-frontend/src/app/ran-control/ran-control.component.ts @@ -20,15 +20,17 @@ 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); - } - ); + ); } }); } diff --git a/webapp-frontend/src/app/ran-control/ran-control.datasource.ts b/webapp-frontend/src/app/ran-control/ran-control.datasource.ts index b23a3cd2..503f459c 100644 --- a/webapp-frontend/src/app/ran-control/ran-control.datasource.ts +++ b/webapp-frontend/src/app/ran-control/ran-control.datasource.ts @@ -43,9 +43,9 @@ export class RANControlDataSource extends DataSource { 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); diff --git a/webapp-frontend/src/app/rd.component.html b/webapp-frontend/src/app/rd.component.html index 7146b37f..85cbbcd8 100644 --- a/webapp-frontend/src/app/rd.component.html +++ b/webapp-frontend/src/app/rd.component.html @@ -73,15 +73,15 @@ viewBox="31.5 30 49.9 32"> + transform="translate(31.5 58)" /> + transform="translate(31.5 44)" /> + transform="translate(31.5 30)" /> - + -

+
+ Select RIC Instance + + + + {{instance.name}} + + + +
Light diff --git a/webapp-frontend/src/app/rd.component.scss b/webapp-frontend/src/app/rd.component.scss index 4a494879..7245077b 100644 --- a/webapp-frontend/src/app/rd.component.scss +++ b/webapp-frontend/src/app/rd.component.scss @@ -363,3 +363,7 @@ mat-drawer-content { width: 100%; } } + +.selector-label { + margin-right: 20px; +} \ No newline at end of file diff --git a/webapp-frontend/src/app/rd.component.ts b/webapp-frontend/src/app/rd.component.ts index 1673280e..16a18495 100644 --- a/webapp-frontend/src/app/rd.component.ts +++ b/webapp-frontend/src/app/rd.component.ts @@ -18,6 +18,10 @@ * ========================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); + } + } diff --git a/webapp-frontend/src/app/rd.module.ts b/webapp-frontend/src/app/rd.module.ts index 440184f8..e9944f1f 100644 --- a/webapp-frontend/src/app/rd.module.ts +++ b/webapp-frontend/src/app/rd.module.ts @@ -17,7 +17,10 @@ * 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] diff --git a/webapp-frontend/src/app/services/ac-xapp/ac-xapp.service.ts b/webapp-frontend/src/app/services/ac-xapp/ac-xapp.service.ts index d25e9db9..6e67b32d 100644 --- a/webapp-frontend/src/app/services/ac-xapp/ac-xapp.service.ts +++ b/webapp-frontend/src/app/services/ac-xapp/ac-xapp.service.ts @@ -18,12 +18,13 @@ * ========================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 { - const url = this.buildPath('version'); + const url = 'api/a1-p/version' return this.httpClient.get(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 { - const url = this.buildPath(this.policyPath, this.acPolicyName); + getPolicy(instanceKey: string): Observable { + const url = this.commonSvc.buildPath(instanceKey, this.component, this.policyPath, this.acPolicyName); return this.httpClient.get(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 { - const url = this.buildPath(this.policyPath, this.acPolicyName); + putPolicy(instanceKey: string, policy: ACAdmissionIntervalControl): Observable { + const url = this.commonSvc.buildPath(instanceKey, this.component, this.policyPath, this.acPolicyName); return this.httpClient.put(url, policy, { observe: 'response' }); } diff --git a/webapp-frontend/src/app/services/app-mgr/app-mgr.service.ts b/webapp-frontend/src/app/services/app-mgr/app-mgr.service.ts index afe7a657..315139e1 100644 --- a/webapp-frontend/src/app/services/app-mgr/app-mgr.service.ts +++ b/webapp-frontend/src/app/services/app-mgr/app-mgr.service.ts @@ -17,45 +17,54 @@ * 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 { - return this.httpClient.get(this.basePath + '/xapps/list'); + getDeployable(instanceKey: string): Observable { + const url = this.commonSvc.buildPath(instanceKey, this.component, 'xapps', 'list'); + return this.httpClient.get(url); } - getDeployed(): Observable { - return this.httpClient.get(this.basePath + '/xapps'); + getDeployed(instanceKey: string): Observable { + const url = this.commonSvc.buildPath(instanceKey, this.component, 'xapps'); + return this.httpClient.get(url); } - deployXapp(name: string): Observable> { + deployXapp(instanceKey: string, name: string): Observable> { 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> { - return this.httpClient.delete((this.basePath + '/xapps'+ '/' + name), { observe: 'response' }); + undeployXapp(instanceKey: string, name: string): Observable> { + const url = this.commonSvc.buildPath(instanceKey, this.component, 'xapps', name); + return this.httpClient.delete(url, { observe: 'response' }); } - getConfig(): Observable{ + getConfig(instanceKey: string): Observable { + // For demo purpose, pull example config from local return this.httpClient.get("/assets/mockdata/config.json"); - //return this.httpClient.get((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(url); } - putConfig(config: any): Observable> { - return this.httpClient.put((this.basePath + '/config' ), config, { observe: 'response' }); + putConfig(instanceKey: string, config: any): Observable> { + const url = this.commonSvc.buildPath(instanceKey, this.component, 'config'); + return this.httpClient.put(url, config, { observe: 'response' }); } - } diff --git a/webapp-frontend/src/app/services/caas-ingress/caas-ingress.service.ts b/webapp-frontend/src/app/services/caas-ingress/caas-ingress.service.ts index 359a08f0..42cb9280 100644 --- a/webapp-frontend/src/app/services/caas-ingress/caas-ingress.service.ts +++ b/webapp-frontend/src/app/services/caas-ingress/caas-ingress.service.ts @@ -18,10 +18,11 @@ * ========================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 { - const url = this.buildPath('pods', 'cluster', cluster, 'namespace', namespace); + getPodList(instanceKey: string, cluster: string, namespace: string): Observable { + const url = this.commonSvc.buildPath(instanceKey, this.component, 'pods', 'cluster', cluster, 'namespace', namespace); return this.httpClient.get(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 index 00000000..54eaeb56 --- /dev/null +++ b/webapp-frontend/src/app/services/common/common.service.spec.ts @@ -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 index 00000000..82d2cf22 --- /dev/null +++ b/webapp-frontend/src/app/services/common/common.service.ts @@ -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; + } + +} diff --git a/webapp-frontend/src/app/services/e2-mgr/e2-mgr.service.ts b/webapp-frontend/src/app/services/e2-mgr/e2-mgr.service.ts index b3388cf6..dc088a21 100644 --- a/webapp-frontend/src/app/services/e2-mgr/e2-mgr.service.ts +++ b/webapp-frontend/src/app/services/e2-mgr/e2-mgr.service.ts @@ -17,12 +17,13 @@ * 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 { - const url = this.basePath + 'version'; + getVersion(instanceKey: string): Observable { + const url = 'api/e2mgr/version'; return this.httpClient.get(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> { - return this.httpClient.get>(this.basePath + 'ran'); + getRan(instanceKey: string): Observable> { + const url = this.commonSvc.buildPath(instanceKey, this.component, 'nodeb', 'ran'); + return this.httpClient.get>(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> { - return this.httpClient.post(this.basePath + 'endc-setup', req, { observe: 'response' }); + endcSetup(instanceKey: string, req: E2SetupRequest): Observable> { + 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> { - return this.httpClient.post(this.basePath + 'x2-setup', req, { observe: 'response' }); + x2Setup(instanceKey: string, req: E2SetupRequest): Observable> { + 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 { - return this.httpClient.put((this.basePath + 'shutdown'), { observe: 'body' }); + nodebPut(instanceKey: string): Observable { + 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 index 00000000..f00b02c1 --- /dev/null +++ b/webapp-frontend/src/app/services/instance-selector/instance-selector.service.spec.ts @@ -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 index 00000000..202aa668 --- /dev/null +++ b/webapp-frontend/src/app/services/instance-selector/instance-selector.service.ts @@ -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 = new BehaviorSubject(''); + private instanceArray: Observable; + private basePath = 'api/admin/instance'; + + constructor(private httpClient: HttpClient) { } + + getInstanceArray(): Observable { + if (this.instanceArray) { + return this.instanceArray; + } + return this.instanceArray = this.httpClient.get(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 { + return this.selectedInstanceKey; + } + + updateSelectedInstance(instanceKey: string) { + this.selectedInstanceKey.next(instanceKey) + } + +}