limitations under the License.
========================LICENSE_END===================================
-->
-<div class="catalog__section">
- <h3 class="rd-global-page-title">xApp Catalog</h3>
-
- <table mat-table [dataSource]="dataSource" matSort class="catalog-table mat-elevation-z8">
-
- <ng-container matColumnDef="name">
- <mat-header-cell *matHeaderCellDef mat-sort-header> App Name </mat-header-cell>
- <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
- </ng-container>
-
- <ng-container matColumnDef="version">
- <mat-header-cell *matHeaderCellDef mat-sort-header> Version </mat-header-cell>
- <mat-cell *matCellDef="let element"> {{element.version}} </mat-cell>
- </ng-container>
-
- <ng-container matColumnDef="action">
- <mat-header-cell *matHeaderCellDef> Action </mat-header-cell>
- <mat-cell *matCellDef="let element">
- <div class="catalog-button-row">
- <button mat-icon-button (click)="onConfigureApp(element)">
- <mat-icon matTooltip="Adjust settings">settings</mat-icon>
- </button>
- <button mat-icon-button (click)="onDeployApp(element)">
- <mat-icon matTooltip="Deploy app">cloud_upload</mat-icon>
- </button>
- </div>
- </mat-cell>
- </ng-container>
-
- <ng-container matColumnDef="noRecordsFound">
- <mat-footer-cell *matFooterCellDef>No records found.</mat-footer-cell>
- </ng-container>
-
- <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
- <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
- <mat-footer-row *matFooterRowDef="['noRecordsFound']" [ngClass]="{'display-none': dataSource.rowCount > 0}"></mat-footer-row>
-
- </table>
-
- <div class="spinner-container" *ngIf="dataSource.loading$ | async">
- <mat-spinner diameter=50></mat-spinner>
- </div>
+ <div class="catalog__section">
+ <h3 class="rd-global-page-title">xApp Catalog</h3>
+
+ <button mat-flat-button color="primary" class="right-button" (click)="onboard()">Onboard Xapp</button>
+
+ <table mat-table [dataSource]="dataSource" matSort class="catalog-table mat-elevation-z8">
+
+ <ng-container matColumnDef="name">
+ <mat-header-cell *matHeaderCellDef mat-sort-header> App Name </mat-header-cell>
+ <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
+ </ng-container>
+
+ <ng-container matColumnDef="version">
+ <mat-header-cell *matHeaderCellDef mat-sort-header> Version </mat-header-cell>
+ <mat-cell *matCellDef="let element"> {{element.version}} </mat-cell>
+ </ng-container>
+
+ <ng-container matColumnDef="action">
+ <mat-header-cell *matHeaderCellDef> Action </mat-header-cell>
+ <mat-cell *matCellDef="let element">
+ <div class="catalog-button-row">
+ <button mat-icon-button (click)="onConfigureApp(element)">
+ <mat-icon matTooltip="Adjust settings">settings</mat-icon>
+ </button>
+ <button mat-icon-button (click)="onDeployApp(element)">
+ <mat-icon matTooltip="Deploy app">cloud_upload</mat-icon>
+ </button>
+ </div>
+ </mat-cell>
+ </ng-container>
-</div>
+ <ng-container matColumnDef="noRecordsFound">
+ <mat-footer-cell *matFooterCellDef>No records found.</mat-footer-cell>
+ </ng-container>
+
+ <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
+ <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
+ <mat-footer-row *matFooterRowDef="['noRecordsFound']" [ngClass]="{'display-none': dataSource.rowCount > 0}"></mat-footer-row>
+
+ </table>
+
+ <div class="spinner-container" *ngIf="dataSource.loading$ | async">
+ <mat-spinner diameter=50></mat-spinner>
+ </div>
+
+ </div>
import { UiService } from '../services/ui/ui.service';
import { AppConfigurationComponent } from './../app-configuration/app-configuration.component';
import { ConfirmDialogService } from '../services/ui/confirm-dialog.service';
+import { OnboardComponent } from './../onboard/onboard.component';
import { NotificationService } from '../services/ui/notification.service';
import { CatalogDataSource } from './catalog.datasource';
);
}
+ onboard(): void {
+ if (this.darkMode) {
+ this.panelClass = 'dark-theme';
+ } else {
+ this.panelClass = '';
+ }
+ const dialogRef = this.dialog.open(OnboardComponent, {
+ panelClass: this.panelClass,
+ width: '400px',
+ maxHeight: '1000px',
+ position: {
+ top: '10%'
+ },
+ data: {
+ instanceKey: this.instanceKey
+ }
+
+ });
+ }
+
}
--- /dev/null
+<!--
+ ========================LICENSE_START=================================
+ O-RAN-SC
+ %%
+ Copyright (C) 2020 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===================================
+ -->
+
+
+
+<mat-tab-group>
+ <mat-tab label="From Local">
+ <input #configInput type="file" (change)="selectConfigFile($event)" style="display:none;" ngModel />
+ <mat-form-field class="example-full-width" (click)="configInput.click()">
+ <input matInput placeholder="Choose config file" value="{{configInput.value.substr(configInput.value.lastIndexOf('\\')+1)}}">
+ <mat-icon matSuffix>folder_open</mat-icon>
+ </mat-form-field>
+ <input #schemaInput type="file" (change)="selectControlsSchema($event)" style="display:none;" ngModel />
+ <mat-form-field class="example-full-width" (click)="schemaInput.click()">
+ <input matInput placeholder="Choose control schema" value="{{schemaInput.value.substr(schemaInput.value.lastIndexOf('\\')+1)}}">
+ <mat-icon matSuffix>folder_open</mat-icon>
+ </mat-form-field>
+ <div class="modal-footer justify-content-center">
+ <button mat-button class="mat-raised-button mat-primary" (click)="uploadFromLocal()">upload</button>
+ <button mat-button class="mat-raised-button" [mat-dialog-close]="false">Cancel</button>
+ </div>
+
+ </mat-tab>
+
+ <mat-tab label="From URL">
+ <form [formGroup]="urlOnboardForm">
+ <mat-form-field >
+ <mat-label>config file URL</mat-label>
+ <input matInput formControlName="configURL">
+ </mat-form-field>
+ <mat-form-field >
+ <mat-label>control schema URL</mat-label>
+ <input matInput formControlName="schemaURL">
+ </mat-form-field>
+ <div class="modal-footer justify-content-center">
+ <button mat-button class="mat-raised-button mat-primary" [disabled]="!urlOnboardForm.valid" (click)="uploadFromURL(urlOnboardForm.value)" >upload</button>
+ <button mat-button class="mat-raised-button" [mat-dialog-close]="false">Cancel</button>
+ </div>
+ </form>
+ </mat-tab>
+
+</mat-tab-group>
+
--- /dev/null
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 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===================================
+ */
+
+.mat-raised-button {
+ margin-right: 5px;
+}
\ No newline at end of file
--- /dev/null
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { OnboardComponent } from './onboard.component';
+
+describe('OnboardComponent', () => {
+ let component: OnboardComponent;
+ let fixture: ComponentFixture<OnboardComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ OnboardComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OnboardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 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 { Component, Inject, OnInit } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+import { finalize } from 'rxjs/operators';
+import { XappOnboarderService } from '../services/xapp-onboarder/xapp-onboarder.service';
+import { ErrorDialogService } from '../services/ui/error-dialog.service';
+import { LoadingDialogService } from '../services/ui/loading-dialog.service';
+import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
+import { NotificationService } from '../services/ui/notification.service';
+import { FormControl, FormGroup, Validators } from '@angular/forms';
+
+@Component({
+ selector: 'rd-onboard',
+ templateUrl: './onboard.component.html',
+ styleUrls: ['./onboard.component.scss']
+})
+export class OnboardComponent implements OnInit {
+
+ private loadingSubject = new BehaviorSubject<boolean>(false);
+ public loading$ = this.loadingSubject.asObservable();
+ public urlOnboardForm: FormGroup;
+
+ constructor(
+ private dialogRef: MatDialogRef<OnboardComponent>,
+ private xappOnboarderService: XappOnboarderService,
+ private errorDiaglogService: ErrorDialogService,
+ private loadingDialogService: LoadingDialogService,
+ private notificationService: NotificationService,
+ @Inject(MAT_DIALOG_DATA) private data
+ ) { }
+
+ ngOnInit(): void {
+ this.urlOnboardForm = new FormGroup({
+ configURL: new FormControl('', [Validators.required]),
+ schemaURL: new FormControl('', [Validators.required])
+ })
+ }
+ ;
+ configFile: File;
+ controlsSchema: File;
+ descriptor = {
+ "config-file.json": {},
+ "controls-schema.json": {}
+ }
+ descriptor_url = {
+ "config-file.json_url": "",
+ "controls-schema.json_url": ""
+ }
+
+ uploadFromLocal() {
+ this.loadingDialogService.startLoading('Onboarding xApp');
+ this.xappOnboarderService.onboardXappFile(this.descriptor, this.data.instanceKey)
+ .pipe(
+ finalize(() => {
+ this.loadingDialogService.stopLoading();
+ this.dialogRef.close();
+ })
+ )
+ .subscribe(
+ (response: HttpResponse<Object>) => {
+ this.notificationService.success('Onboard succeeded!');
+ },
+ ((her: HttpErrorResponse) => {
+ let msg = her.message;
+ if (her.error && her.error.message) {
+ msg = her.error.message;
+ }
+ this.notificationService.warn('Onboard failed: ' + msg);
+ })
+ );
+ }
+
+ uploadFromURL(data) {
+ this.descriptor_url["config-file.json_url"] = data.configURL;
+ this.descriptor_url["controls-schema.json_url"] = data.schemaURL;
+ this.loadingDialogService.startLoading('Onboarding xApp');
+ this.xappOnboarderService.onboardXappURL(this.descriptor_url, this.data.instanceKey)
+ .pipe(
+ finalize(() => {
+ this.loadingDialogService.stopLoading();
+ this.dialogRef.close();
+ })
+ )
+ .subscribe(
+ (response: HttpResponse<Object>) => {
+ this.notificationService.success('Onboard succeeded!');
+ },
+ ((her: HttpErrorResponse) => {
+ let msg = her.message;
+ if (her.error && her.error.message) {
+ msg = her.error.message;
+ }
+ this.notificationService.warn('Onboard failed: ' + msg);
+ })
+ );
+ }
+
+
+ selectConfigFile(event) {
+ this.configFile = event.target.files[0];
+ let fileReader = new FileReader();
+ fileReader.onload = (e) => {
+ this.descriptor["config-file.json"] = JSON.parse(fileReader.result as string);
+ }
+ fileReader.readAsText(this.configFile);
+ }
+
+ selectControlsSchema(event) {
+ this.controlsSchema = event.target.files[0];
+ let fileReader = new FileReader();
+ fileReader.onload = (e) => {
+ this.descriptor["controls-schema.json"] = JSON.parse(fileReader.result as string);
+ }
+ fileReader.readAsText(this.controlsSchema);
+
+ }
+}
import { ErrorDialogComponent } from './ui/error-dialog/error-dialog.component';
import { FooterComponent } from './footer/footer.component';
import { InstanceSelectorDialogComponent } from './ui/instance-selector-dialog/instance-selector-dialog.component';
+import { OnboardComponent } from './onboard/onboard.component';
import { LoadingDialogComponent } from './ui/loading-dialog/loading-dialog.component';
import { MainComponent } from './main/main.component';
import { PlatformComponent } from './platform/platform.component';
import { InstanceSelectorService } from './services/instance-selector/instance-selector.service';
import { InstanceSelectorDialogService } from './services/ui/instance-selector-dialog.service';
import { UiService } from './services/ui/ui.service';
+import { XappOnboarderService } from './services/xapp-onboarder/xapp-onboarder.service';
@NgModule({
StatsComponent,
StatsDialogComponent,
UserComponent,
- InstanceSelectorDialogComponent
+ InstanceSelectorDialogComponent,
+ OnboardComponent
],
imports: [
BrowserModule,
ErrorDialogService,
InstanceSelectorService,
InstanceSelectorDialogService,
- UiService
+ UiService,
+ XappOnboarderService
],
bootstrap: [RdComponent]
})
--- /dev/null
+/*-
+ * ========================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 { XappOnboarderService } from './xapp-onboarder.service';
+
+describe('XappOnboarderService', () => {
+ let service: XappOnboarderService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(XappOnboarderService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
--- /dev/null
+/*-
+ * ========================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, HttpResponse } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { DashboardService } from '../dashboard/dashboard.service';
+@Injectable({
+ providedIn: 'root'
+})
+export class XappOnboarderService {
+
+ private component = 'xappobrd';
+
+ constructor(
+ private dashboardSvc: DashboardService,
+ private httpClient: HttpClient
+ ) { }
+
+ onboardXappFile(descriptor: any, instanceKey: string): Observable<HttpResponse<Object>> {
+ const path = this.dashboardSvc.buildPath(this.component, instanceKey, 'onboard');
+ return this.httpClient.post(path, descriptor, { observe: 'response' });
+ }
+
+ onboardXappURL(descriptor_remote: any, instanceKey: string): Observable<HttpResponse<Object>> {
+ const path = this.dashboardSvc.buildPath(this.component, instanceKey, 'onboard','download');
+ return this.httpClient.post(path, descriptor_remote, { observe: 'response' });
+ }
+}
RIC Dashboard Release Notes
===========================
-Version 2.1.0, 8 Jun 2020
+Version 2.1.0, 17 Aug 2020
--------------------------
* Add Xapp Onboarder client to backend (`OAM-108 <https://jira.o-ran-sc.org/browse/OAM-108>`_)
+* Add Xapp Onboarder frontend UI (`OAM-108 <https://jira.o-ran-sc.org/browse/OAM-108>`_)
Version 2.0.3, 3 Jun 2020
--------------------------