From 869354bad80ea5db92893ad3017eaf9c421c0731 Mon Sep 17 00:00:00 2001 From: ychacon Date: Mon, 1 Mar 2021 09:58:28 +0100 Subject: [PATCH] Create feature module for Enrichment coordinator - Create component for EI Jobs - Add module for EI coordinator Issue-ID: NONRTRIC-453 Signed-off-by: ychacon Change-Id: I01ad3581e536e85631237e661050c9d319a51215 --- webapp-frontend/src/app/app.module.ts | 7 +- .../ei-coordinator/ei-coordinator.component.html | 66 +-------- .../ei-coordinator/ei-coordinator.component.scss | 30 +--- .../ei-coordinator.component.spec.ts | 152 ++++++------------- .../app/ei-coordinator/ei-coordinator.component.ts | 96 ++---------- .../app/ei-coordinator/ei-coordinator.module.ts | 63 ++++++++ .../app/ei-coordinator/ei-job.datasource.spec.ts | 6 +- .../src/app/ei-coordinator/ei-job.datasource.ts | 6 +- .../ei-coordinator/ei-producer.datasource.spec.ts | 12 +- .../app/ei-coordinator/ei-producer.datasource.ts | 4 +- .../jobs-list/jobs-list.component.html | 78 ++++++++++ .../jobs-list/jobs-list.component.scss | 31 ++++ .../jobs-list/jobs-list.component.spec.ts | 161 +++++++++++++++++++++ .../jobs-list/jobs-list.component.ts | 121 ++++++++++++++++ .../producers-list/producers-list.component.html | 78 ++++++---- .../producers-list/producers-list.component.scss | 35 ++++- .../producers-list.component.spec.ts | 23 ++- .../producers-list/producers-list.component.ts | 42 ++++-- 18 files changed, 656 insertions(+), 355 deletions(-) create mode 100644 webapp-frontend/src/app/ei-coordinator/ei-coordinator.module.ts create mode 100644 webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.html create mode 100644 webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.scss create mode 100644 webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.spec.ts create mode 100644 webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.ts diff --git a/webapp-frontend/src/app/app.module.ts b/webapp-frontend/src/app/app.module.ts index 20fe471..6ea9f03 100644 --- a/webapp-frontend/src/app/app.module.ts +++ b/webapp-frontend/src/app/app.module.ts @@ -57,6 +57,7 @@ import { FooterComponent } from './footer/footer.component'; import { MainComponent } from './main/main.component'; import { MaterialDesignFrameworkModule } from 'angular6-json-schema-form'; import { PolicyModule } from './policy/policy.module' +import { EiCoordinatorModule } from './ei-coordinator/ei-coordinator.module' import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { SidenavListComponent } from './navigation/sidenav-list/sidenav-list.component'; @@ -64,11 +65,9 @@ import { UiService } from './services/ui/ui.service'; import { CookieService } from 'ngx-cookie-service'; import { NodeModulesComponent } from './node-modules/node-modules.component'; import { EICardComponent } from './ui/ei-card/ei-card.component'; -import { EICoordinatorComponent } from './ei-coordinator/ei-coordinator.component'; import { HttpMockRequestInterceptor } from './interceptor.mock'; import { environment } from 'src/environments/environment'; import { HttpRequestInterceptor } from './interceptor'; -import { ProducersListComponent } from './ei-coordinator/producers-list/producers-list.component'; export const isMock = environment.mock; @@ -77,13 +76,11 @@ export const isMock = environment.mock; AppComponent, ConfirmDialogComponent, EICardComponent, - EICoordinatorComponent, ErrorDialogComponent, FooterComponent, MainComponent, NodeModulesComponent, SidenavListComponent, - ProducersListComponent, ], imports: [ AppRoutingModule, @@ -118,6 +115,7 @@ export const isMock = environment.mock; MaterialDesignFrameworkModule, MDBBootstrapModule.forRoot(), PolicyModule, + EiCoordinatorModule, ReactiveFormsModule, ToastrModule.forRoot(), ], @@ -146,7 +144,6 @@ export const isMock = environment.mock; providers: [ CookieService, ErrorDialogService, - ProducersListComponent, UiService, { provide: HTTP_INTERCEPTORS, diff --git a/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.html b/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.html index 57a87f9..3162772 100644 --- a/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.html +++ b/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.html @@ -21,7 +21,7 @@ limitations under the License.
Enrichment Information Coordinator
-
@@ -29,68 +29,8 @@ limitations under the License.

Producers

- +

Jobs

- -
- - - -
-
- - - Job ID - -
-
-
- {{eiJob.ei_job_identity}} -
- - -
-
- - - Type ID - -
-
-
- {{this.getJobTypeId(eiJob)}} -
- - -
-
- - - Owner - -
-
-
- {{this.getJobOwner(eiJob)}} -
- - -
-
- - - Target URI - -
-
-
- {{eiJob.target_uri}} -
- - -
-
\ No newline at end of file + \ No newline at end of file diff --git a/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.scss b/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.scss index 109d883..31a6b18 100644 --- a/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.scss +++ b/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.scss @@ -17,19 +17,6 @@ * limitations under the License. * ========================LICENSE_END=================================== */ - -.ei-coordinator-table { - width: 100%; - min-height: 150px; - margin-top: 10px; - margin-bottom: 10px; - background-color: transparent; -} - -.table-dark { - background-color: #2d2d3d; -} - .display-none { display: none; } @@ -38,19 +25,4 @@ display: flex; justify-content: space-between; flex-wrap: wrap; -} - -.table-container { - margin-top: 10px; - max-height: 300px; - overflow: auto; -} - -.filter-form { - width: 200px; -} - -.mat-form-field { - font-size: 14px; - width: 100%; -} +} \ No newline at end of file diff --git a/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.spec.ts b/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.spec.ts index 5cda8e5..7c50ca0 100644 --- a/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.spec.ts +++ b/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.spec.ts @@ -19,75 +19,50 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations' -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { HarnessLoader } from '@angular/cdk/testing'; import { MatButtonModule } from '@angular/material/button'; import { MatButtonHarness } from '@angular/material/button/testing'; import { MatIconModule } from '@angular/material/icon'; -import { MatInputHarness } from '@angular/material/input/testing' import { MatTableModule } from '@angular/material/table'; -import { MatTableHarness } from '@angular/material/table/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; -import { of } from 'rxjs'; import { EICoordinatorComponent } from './ei-coordinator.component'; -import { EIJobDataSource } from './ei-job.datasource'; import { UiService } from '../services/ui/ui.service'; -import { EIJob } from '../interfaces/ei.types'; import { ProducersListComponent } from './producers-list/producers-list.component'; +import { JobsListComponent } from './jobs-list/jobs-list.component'; describe('EICoordinatorComponent', () => { let component: EICoordinatorComponent; let fixture: ComponentFixture; let loader: HarnessLoader; - let producersListSpy: jasmine.SpyObj; - let jobDataSourceSpy: jasmine.SpyObj; - - const job1 = { - ei_job_identity: 'job1', - ei_type_identity: 'type1', - owner: 'owner1', - target_uri: 'http://one' - } as EIJob; - const job2 = { - ei_job_identity: 'job2', - ei_type_identity: 'type2', - owner: 'owner2', - target_uri: 'http://two' - } as EIJob; beforeEach(async () => { - producersListSpy = jasmine.createSpyObj('producersListSpy', ['refresh']); - jobDataSourceSpy = jasmine.createSpyObj('EIJobDataSource', [ 'loadJobs', 'eiJobs', 'eiJobsSubject' ]); - - jobDataSourceSpy.eiJobsSubject.and.returnValue(of([ job1, job2 ])); await TestBed.configureTestingModule({ imports: [ MatButtonModule, MatIconModule, MatTableModule, - BrowserAnimationsModule, - ReactiveFormsModule + BrowserAnimationsModule ], schemas: [ CUSTOM_ELEMENTS_SCHEMA ], declarations: [ - EICoordinatorComponent + EICoordinatorComponent, + JobsListStubComponent, + ProducerListStubComponent, ], providers: [ - { provide: ProducersListComponent, useValue: producersListSpy }, - { provide: EIJobDataSource, useValue: jobDataSourceSpy }, - UiService, - FormBuilder, + UiService ] }) .compileComponents(); fixture = TestBed.createComponent(EICoordinatorComponent); component = fixture.componentInstance; + fixture.detectChanges(); loader = TestbedHarnessEnvironment.loader(fixture); }); @@ -97,7 +72,7 @@ describe('EICoordinatorComponent', () => { }); describe('#content', () => { - it('should contain refresh button with coorect icon', async () => { + it('should contain refresh button with correct icon', async () => { let refreshButton = await loader.getHarness(MatButtonHarness.with({ selector: '#refreshButton' })); expect(refreshButton).toBeTruthy(); expect(await refreshButton.getText()).toEqual('refresh'); @@ -108,12 +83,9 @@ describe('EICoordinatorComponent', () => { expect(producersTableComponent).toBeTruthy(); }); - it('should contain jobs table with correct columns', async () => { - let producersTable = await loader.getHarness(MatTableHarness.with({ selector: '#jobsTable' })); - let headerRow = (await producersTable.getHeaderRows())[0]; - let headers = await headerRow.getCellTextByColumnName(); - - expect(headers).toEqual({ id: 'Job ID', typeId: 'Type ID', owner: 'Owner', targetUri: 'Target URI' }); + it('should contain jobs table', async () => { + const jobsComponent = fixture.debugElement.nativeElement.querySelector('nrcp-jobs-list'); + expect(jobsComponent).toBeTruthy(); }); it('should set correct dark mode from UIService', () => { @@ -128,79 +100,41 @@ describe('EICoordinatorComponent', () => { it('should refresh tables', async () => { let refreshButton = await loader.getHarness(MatButtonHarness.with({ selector: '#refreshButton' })); + spyOn(component.producersList, 'refresh'); + spyOn(component.jobComponent, 'refresh'); await refreshButton.click(); - expect(producersListSpy.refresh).toHaveBeenCalled(); + expect(component.jobComponent.refresh).toHaveBeenCalled(); + expect(component.producersList.refresh).toHaveBeenCalled(); }); }); - describe('#jobsTable', () => { - const expectedJob1Row = { id: 'job1', typeId: 'type1', owner: 'owner1', targetUri: 'http://one' }; - beforeEach(() => { - const jobs: EIJob[] = [ job1, job2 ]; - jobDataSourceSpy.eiJobsSubject.and.returnValue(of(jobs)); - }); - - it('should contain data after initialization', async () => { - component.ngOnInit(); - const expectedJobRows = [ - expectedJob1Row, - { id: 'job2', typeId: 'type2', owner: 'owner2', targetUri: 'http://two' } - ]; - let jobsTable = await loader.getHarness(MatTableHarness.with({ selector: '#jobsTable' })); - let jobRows = await jobsTable.getRows(); - expect(jobRows.length).toEqual(2); - jobRows.forEach(row => { - row.getCellTextByColumnName().then(values => { - expect(expectedJobRows).toContain(jasmine.objectContaining(values)); - }); - }); - }); - - it('job defaults', async () => { - const jobMissingProperties = { - ei_job_identity: 'job1', - target_uri: 'http://one' - } as EIJob; - const jobs: EIJob[] = [jobMissingProperties]; - jobDataSourceSpy.eiJobsSubject.and.returnValue(of(jobs)); - component.ngOnInit(); - - const expectedJobRow = { id: 'job1', typeId: '< No type >', owner: '< No owner >', targetUri: 'http://one' }; - let jobsTable = await loader.getHarness(MatTableHarness.with({ selector: '#jobsTable' })); - let jobRows = await jobsTable.getRows(); - expect(await jobRows[0].getCellTextByColumnName()).toEqual(expectedJobRow); - }); + @Component({ + selector: 'nrcp-jobs-list', + template: '', + providers: [ + { + provide: JobsListComponent, + useClass: JobsListStubComponent + } + ] + }) + class JobsListStubComponent { + refresh() { } + } + + @Component({ + selector: 'nrcp-producers-list', + template: '', + providers: [ + { + provide: ProducersListComponent, + useClass: ProducerListStubComponent + } + ] + }) + class ProducerListStubComponent { + refresh() { } + } - it('filtering', async () => { - component.ngOnInit(); - let jobsTable = await loader.getHarness(MatTableHarness.with({ selector: '#jobsTable' })); - - let idFilterInput = await loader.getHarness(MatInputHarness.with({ selector: '#jobIdFilter' })); - await idFilterInput.setValue("1"); - let jobRows = await jobsTable.getRows(); - expect(jobRows.length).toEqual(1); - expect(await jobRows[0].getCellTextByColumnName()).toEqual(expectedJob1Row); - - idFilterInput.setValue(''); - let typeIdFilterInput = await loader.getHarness(MatInputHarness.with({ selector: '#jobTypeIdFilter' })); - await typeIdFilterInput.setValue("1"); - jobRows = await jobsTable.getRows(); - expect(jobRows.length).toEqual(1); - - typeIdFilterInput.setValue(''); - let ownerFilterInput = await loader.getHarness(MatInputHarness.with({ selector: '#jobOwnerFilter' })); - await ownerFilterInput.setValue("1"); - jobRows = await jobsTable.getRows(); - expect(jobRows.length).toEqual(1); - expect(await jobRows[0].getCellTextByColumnName()).toEqual(expectedJob1Row); - - ownerFilterInput.setValue(''); - let targetUriFilterInput = await loader.getHarness(MatInputHarness.with({ selector: '#jobTargetUriFilter' })); - await targetUriFilterInput.setValue("one"); - jobRows = await jobsTable.getRows(); - expect(jobRows.length).toEqual(1); - expect(await jobRows[0].getCellTextByColumnName()).toEqual(expectedJob1Row); - }); - }); }); diff --git a/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.ts b/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.ts index 79b7129..d531948 100644 --- a/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.ts +++ b/webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.ts @@ -17,114 +17,40 @@ * limitations under the License. * ========================LICENSE_END=================================== */ -import { Component, OnInit } from '@angular/core'; -import { Sort } from '@angular/material/sort'; -import { FormBuilder, FormGroup, AbstractControl } from '@angular/forms'; -import { MatTableDataSource } from '@angular/material/table'; +import { Component, OnInit, ViewChild } from '@angular/core'; -import { EIJob } from '../interfaces/ei.types'; -import { EIJobDataSource } from './ei-job.datasource'; import { UiService } from '../services/ui/ui.service'; +import { JobsListComponent } from './jobs-list/jobs-list.component'; import { ProducersListComponent } from './producers-list/producers-list.component'; @Component({ selector: 'nrcp-ei-coordinator', templateUrl: './ei-coordinator.component.html', - styleUrls: ['./ei-coordinator.component.scss'] + styleUrls: ['./ei-coordinator.component.scss'], + providers: [ + ProducersListComponent, + JobsListComponent + ] }) export class EICoordinatorComponent implements OnInit { darkMode: boolean; - formGroup: FormGroup; - jobsDataSource: MatTableDataSource = new MatTableDataSource(); + @ViewChild(ProducersListComponent) producersList: ProducersListComponent; + @ViewChild(JobsListComponent) jobComponent: JobsListComponent; - readonly jobsFormControl: AbstractControl; constructor( - private producersList: ProducersListComponent, - private eiJobsDataSource: EIJobDataSource, - private ui: UiService, - private formBuilder: FormBuilder) { - this.formGroup = formBuilder.group({ filter: [""] }); - - this.jobsFormControl = formBuilder.group({ - id: '', - typeId: '', - owner: '', - targetUri:'' - }); + private ui: UiService) { } ngOnInit() { - this.eiJobsDataSource.loadJobs(); - - this.eiJobsDataSource.eiJobsSubject().subscribe((data) => { - this.jobsDataSource.data = data; - }); - - this.jobsFormControl.valueChanges.subscribe(value => { - const filter = {...value, id: value.id.trim().toLowerCase()} as string; - this.jobsDataSource.filter = filter; - }); - - this.jobsDataSource.filterPredicate = ((data: EIJob, filter) => { - return this.isDataIncluding(data.ei_job_identity, filter.id) - && this.isDataIncluding(data.target_uri, filter.targetUri) - && this.isDataIncluding(data.owner, filter.owner) - && this.isDataIncluding(data.ei_type_identity, filter.typeId); - }) as (data: EIJob, filter: any) => boolean; - this.ui.darkModeState.subscribe((isDark) => { this.darkMode = isDark; }); } - sortJobs(sort: Sort){ - const data = this.jobsDataSource.data - data.sort((a: EIJob, b: EIJob) => { - const isAsc = sort.direction === 'asc'; - switch (sort.active) { - case 'id': return this.compare(a.ei_job_identity, b.ei_job_identity, isAsc); - case 'typeId': return this.compare(a.ei_type_identity, b.ei_type_identity, isAsc); - case 'owner': return this.compare(a.owner, b.owner, isAsc); - case 'targetUri': return this.compare(a.target_uri, b.owner, isAsc); - default: return 0; - } - }); - this.jobsDataSource.data = data; - } - - compare(a: any, b: any, isAsc: boolean) { - return (a < b ? -1 : 1) * (isAsc ? 1 : -1); - } - - stopSort(event: any){ - event.stopPropagation(); - } - - isDataIncluding(data: string, filter: string) : boolean { - return !filter || data.toLowerCase().includes(filter); - } - - getJobTypeId(eiJob: EIJob): string { - if (eiJob.ei_type_identity) { - return eiJob.ei_type_identity; - } - return '< No type >'; - } - - getJobOwner(eiJob: EIJob): string { - if (eiJob.owner) { - return eiJob.owner; - } - return '< No owner >'; - } refreshTables() { - this.eiJobsDataSource.loadJobs(); - this.eiJobsDataSource.eiJobsSubject().subscribe((data) => { - this.jobsDataSource.data = data; - }); - + this.jobComponent.refresh(); this.producersList.refresh(); } } diff --git a/webapp-frontend/src/app/ei-coordinator/ei-coordinator.module.ts b/webapp-frontend/src/app/ei-coordinator/ei-coordinator.module.ts new file mode 100644 index 0000000..1e38575 --- /dev/null +++ b/webapp-frontend/src/app/ei-coordinator/ei-coordinator.module.ts @@ -0,0 +1,63 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2021 AT&T Intellectual Property + * Modifications Copyright (C) 2019 Nordix Foundation + * Modifications Copyright (C) 2020 Nordix Foundation + * %% + * 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { EICoordinatorComponent } from './ei-coordinator.component'; +import { ProducersListComponent } from './producers-list/producers-list.component'; +import { JobsListComponent } from './jobs-list/jobs-list.component'; +import { MatTableModule } from '@angular/material/table'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatSortModule } from '@angular/material/sort'; +import { MatButtonModule } from '@angular/material/button'; +import { FlexLayoutModule } from '@angular/flex-layout'; + + +const routes: Routes = [ + { path: 'ei-coordinator', component: EICoordinatorComponent } +]; + +@NgModule({ + declarations: [ + EICoordinatorComponent, + ProducersListComponent, + JobsListComponent + ], + imports: [ + CommonModule, + FlexLayoutModule, + MatTableModule, + MatIconModule, + MatInputModule, + ReactiveFormsModule, + FormsModule, + MatSortModule, + MatButtonModule, + RouterModule.forChild(routes) + ], + exports: [ + EICoordinatorComponent + ] +}) +export class EiCoordinatorModule { } diff --git a/webapp-frontend/src/app/ei-coordinator/ei-job.datasource.spec.ts b/webapp-frontend/src/app/ei-coordinator/ei-job.datasource.spec.ts index c50f69e..2487088 100644 --- a/webapp-frontend/src/app/ei-coordinator/ei-job.datasource.spec.ts +++ b/webapp-frontend/src/app/ei-coordinator/ei-job.datasource.spec.ts @@ -29,12 +29,12 @@ describe('EIJobDataSource', () => { let dataSource: EIJobDataSource; let eiServiceSpy: any; - const job = { ei_job_identity: '1', ei_job_data: 'data', ei_type_identity: 'Type ID 1', target_uri: 'hhtp://url', owner: 'owner'}; + const job = { ei_job_identity: '1', ei_job_data: 'data', ei_type_identity: 'Type ID 1', target_uri: 'hhtp://url', owner: 'owner' }; beforeEach(() => { eiServiceSpy = jasmine.createSpyObj('EIService', ['getProducerIds', 'getJobsForProducer']); - eiServiceSpy.getProducerIds.and.returnValue(of([ 'producer1', 'producer2'])); + eiServiceSpy.getProducerIds.and.returnValue(of(['producer1', 'producer2'])); eiServiceSpy.getJobsForProducer.and.returnValue(of([job])); TestBed.configureTestingModule({ imports: [ToastrModule.forRoot()], @@ -52,7 +52,7 @@ describe('EIJobDataSource', () => { it('#getJobs', () => { dataSource.loadJobs(); const actualJobs: EIJob[] = dataSource.eiJobs(); - expect(actualJobs).toEqual([ job, job ]); + expect(actualJobs).toEqual([job, job]); expect(dataSource.rowCount).toEqual(2); }); }); diff --git a/webapp-frontend/src/app/ei-coordinator/ei-job.datasource.ts b/webapp-frontend/src/app/ei-coordinator/ei-job.datasource.ts index ac4109d..b008f80 100644 --- a/webapp-frontend/src/app/ei-coordinator/ei-job.datasource.ts +++ b/webapp-frontend/src/app/ei-coordinator/ei-job.datasource.ts @@ -59,16 +59,16 @@ export class EIJobDataSource { this.loadingSubject.next(true); this.jobs = []; this.eiSvc.getProducerIds().pipe( - mergeMap(prodIds => + mergeMap(prodIds => forkJoin(prodIds.map(id => this.eiSvc.getJobsForProducer(id)))), mergeMap(result => result), finalize(() => this.loadingSubject.next(false)) ).subscribe(result => { this.jobs = this.jobs.concat(result); this.jobsSubject.next(this.jobs); - } ); + }); this.rowCount = this.jobs.length; } - + } diff --git a/webapp-frontend/src/app/ei-coordinator/ei-producer.datasource.spec.ts b/webapp-frontend/src/app/ei-coordinator/ei-producer.datasource.spec.ts index c519b12..d1f22e2 100644 --- a/webapp-frontend/src/app/ei-coordinator/ei-producer.datasource.spec.ts +++ b/webapp-frontend/src/app/ei-coordinator/ei-producer.datasource.spec.ts @@ -30,10 +30,10 @@ describe('EIProducerDataSource', () => { let eiServiceSpy: any; let producer1 = { - supported_ei_types: [ 'type1', 'type2' ] + supported_ei_types: ['type1', 'type2'] } as ProducerRegistrationInfo; let producer2 = { - supported_ei_types: [ 'type3', 'type4' ] + supported_ei_types: ['type3', 'type4'] } as ProducerRegistrationInfo; let producerStatus1 = { operational_state: OperationalState.ENABLED @@ -44,19 +44,19 @@ describe('EIProducerDataSource', () => { let expectedProducer1 = { ei_producer_id: 'producer1', - ei_producer_types: [ 'type1', 'type2' ], + ei_producer_types: ['type1', 'type2'], status: 'ENABLED' } as EIProducer; let expectedProducer2 = { ei_producer_id: 'producer2', - ei_producer_types: [ 'type3', 'type4' ], + ei_producer_types: ['type3', 'type4'], status: 'DISABLED' } as EIProducer; beforeEach(() => { eiServiceSpy = jasmine.createSpyObj('EIService', ['getProducerIds', 'getProducer', 'getProducerStatus']); - eiServiceSpy.getProducerIds.and.returnValue(of([ 'producer1', 'producer2'])); + eiServiceSpy.getProducerIds.and.returnValue(of(['producer1', 'producer2'])); eiServiceSpy.getProducer.and.returnValues(of(producer1), of(producer2)); eiServiceSpy.getProducerStatus.and.returnValues(of(producerStatus1), of(producerStatus2)); TestBed.configureTestingModule({ @@ -75,7 +75,7 @@ describe('EIProducerDataSource', () => { it('#loadProducers', () => { dataSource.loadProducers(); const actualProducers: EIProducer[] = dataSource.eiProducers(); - expect(actualProducers).toEqual([ expectedProducer1, expectedProducer2 ]); + expect(actualProducers).toEqual([expectedProducer1, expectedProducer2]); expect(dataSource.rowCount).toEqual(2); }); }); diff --git a/webapp-frontend/src/app/ei-coordinator/ei-producer.datasource.ts b/webapp-frontend/src/app/ei-coordinator/ei-producer.datasource.ts index 439352d..cb4809c 100644 --- a/webapp-frontend/src/app/ei-coordinator/ei-producer.datasource.ts +++ b/webapp-frontend/src/app/ei-coordinator/ei-producer.datasource.ts @@ -59,7 +59,7 @@ export class EIProducerDataSource { this.producers = []; this.eiSvc.getProducerIds().pipe( - mergeMap(prodIds => + mergeMap(prodIds => forkJoin(prodIds.map(id => { return forkJoin([ of(id), @@ -67,7 +67,7 @@ export class EIProducerDataSource { this.eiSvc.getProducerStatus(id) ]) }) - )), + )), finalize(() => this.loadingSubject.next(false)) ).subscribe(result => { this.producers = result.map(producer => { diff --git a/webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.html b/webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.html new file mode 100644 index 0000000..840cc95 --- /dev/null +++ b/webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.html @@ -0,0 +1,78 @@ + +
+ + + +
+
+ + + Job ID + +
+
+
+ {{eiJob.ei_job_identity}} +
+ + +
+
+ + + Type ID + +
+
+
+ {{this.getJobTypeId(eiJob)}} +
+ + +
+
+ + + Owner + +
+
+
+ {{this.getJobOwner(eiJob)}} +
+ + +
+
+ + + Target URI + +
+
+
+ {{eiJob.target_uri}} +
+ + +
+
\ No newline at end of file diff --git a/webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.scss b/webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.scss new file mode 100644 index 0000000..d0f2ee7 --- /dev/null +++ b/webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.scss @@ -0,0 +1,31 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2021 Nordix Foundation + * %% + * 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=================================== + */ +.ei-coordinator-table { + width: 100%; + min-height: 150px; + margin-top: 10px; + margin-bottom: 10px; + background-color: transparent; +} + +.mat-form-field { + font-size: 14px; + width: 100%; +} \ No newline at end of file diff --git a/webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.spec.ts b/webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.spec.ts new file mode 100644 index 0000000..24750eb --- /dev/null +++ b/webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.spec.ts @@ -0,0 +1,161 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2021 Nordix Foundation + * %% + * 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 { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; +import { MatInputHarness } from '@angular/material/input/testing'; +import { MatTableModule } from '@angular/material/table'; +import { MatTableHarness } from '@angular/material/table/testing'; +import { of } from 'rxjs/observable/of'; +import { EIJob } from 'src/app/interfaces/ei.types'; +import { UiService } from 'src/app/services/ui/ui.service'; +import { EIJobDataSource } from '../ei-job.datasource'; + +import { JobsListComponent } from './jobs-list.component'; + +describe('JobsListComponent', () => { + let component: JobsListComponent; + let fixture: ComponentFixture; + let loader: HarnessLoader; + let eiJobComponent: jasmine.SpyObj; + let jobDataSourceSpy: jasmine.SpyObj; + + const job1 = { + ei_job_identity: 'job1', + ei_type_identity: 'type1', + owner: 'owner1', + target_uri: 'http://one' + } as EIJob; + const job2 = { + ei_job_identity: 'job2', + ei_type_identity: 'type2', + owner: 'owner2', + target_uri: 'http://two' + } as EIJob; + + beforeEach(async(() => { + eiJobComponent = jasmine.createSpyObj('producersListSpy', ['refresh']); + jobDataSourceSpy = jasmine.createSpyObj('EIJobDataSource', ['loadJobs', 'eiJobs', 'eiJobsSubject']); + + jobDataSourceSpy.eiJobsSubject.and.returnValue(of([job1, job2])); + + TestBed.configureTestingModule({ + imports: [ + MatTableModule, + ReactiveFormsModule + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ], + declarations: [JobsListComponent], + providers: [ + { provide: EIJobDataSource, useValue: jobDataSourceSpy }, + UiService, + FormBuilder, + ] + }) + .compileComponents(); + })); + + const expectedJob1Row = { id: 'job1', typeId: 'type1', owner: 'owner1', targetUri: 'http://one' }; + + beforeEach(() => { + fixture = TestBed.createComponent(JobsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should contain job table with correct columns', async () => { + let producersTable = await loader.getHarness(MatTableHarness.with({ selector: '#jobsTable' })); + let headerRow = (await producersTable.getHeaderRows())[0]; + let headers = await headerRow.getCellTextByColumnName(); + + expect(headers).toEqual({ id: 'Job ID', typeId: 'Type ID', owner: 'Owner', targetUri: 'Target URI' }); + }); + + it('should contain data after initialization', async () => { + component.ngOnInit(); + const expectedJobRows = [ + expectedJob1Row, + { id: 'job2', typeId: 'type2', owner: 'owner2', targetUri: 'http://two' } + ]; + let jobsTable = await loader.getHarness(MatTableHarness.with({ selector: '#jobsTable' })); + let jobRows = await jobsTable.getRows(); + expect(jobRows.length).toEqual(2); + jobRows.forEach(row => { + row.getCellTextByColumnName().then(values => { + expect(expectedJobRows).toContain(jasmine.objectContaining(values)); + }); + }); + }); + + it('should display default values for non required properties ', async () => { + const jobMissingProperties = { + ei_job_identity: 'job1', + target_uri: 'http://one' + } as EIJob; + const jobs: EIJob[] = [jobMissingProperties]; + jobDataSourceSpy.eiJobsSubject.and.returnValue(of(jobs)); + component.ngOnInit(); + + const expectedJobRow = { id: 'job1', typeId: '< No type >', owner: '< No owner >', targetUri: 'http://one' }; + let jobsTable = await loader.getHarness(MatTableHarness.with({ selector: '#jobsTable' })); + let jobRows = await jobsTable.getRows(); + expect(await jobRows[0].getCellTextByColumnName()).toEqual(expectedJobRow); + }); + + it('filtering', async () => { + component.ngOnInit(); + let jobsTable = await loader.getHarness(MatTableHarness.with({ selector: '#jobsTable' })); + + let idFilterInput = await loader.getHarness(MatInputHarness.with({ selector: '#jobIdFilter' })); + await idFilterInput.setValue("1"); + let jobRows = await jobsTable.getRows(); + expect(jobRows.length).toEqual(1); + expect(await jobRows[0].getCellTextByColumnName()).toEqual(expectedJob1Row); + + idFilterInput.setValue(''); + let typeIdFilterInput = await loader.getHarness(MatInputHarness.with({ selector: '#jobTypeIdFilter' })); + await typeIdFilterInput.setValue("1"); + jobRows = await jobsTable.getRows(); + expect(jobRows.length).toEqual(1); + + typeIdFilterInput.setValue(''); + let ownerFilterInput = await loader.getHarness(MatInputHarness.with({ selector: '#jobOwnerFilter' })); + await ownerFilterInput.setValue("1"); + jobRows = await jobsTable.getRows(); + expect(jobRows.length).toEqual(1); + expect(await jobRows[0].getCellTextByColumnName()).toEqual(expectedJob1Row); + + ownerFilterInput.setValue(''); + let targetUriFilterInput = await loader.getHarness(MatInputHarness.with({ selector: '#jobTargetUriFilter' })); + await targetUriFilterInput.setValue("one"); + jobRows = await jobsTable.getRows(); + expect(jobRows.length).toEqual(1); + expect(await jobRows[0].getCellTextByColumnName()).toEqual(expectedJob1Row); + }); +}); diff --git a/webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.ts b/webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.ts new file mode 100644 index 0000000..5208112 --- /dev/null +++ b/webapp-frontend/src/app/ei-coordinator/jobs-list/jobs-list.component.ts @@ -0,0 +1,121 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2021 Nordix Foundation + * %% + * 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, OnInit } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { Sort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; +import { EIJob } from 'src/app/interfaces/ei.types'; +import { UiService } from 'src/app/services/ui/ui.service'; +import { EIJobDataSource } from '../ei-job.datasource'; + +@Component({ + selector: 'nrcp-jobs-list', + templateUrl: './jobs-list.component.html', + styleUrls: ['./jobs-list.component.scss'] +}) +export class JobsListComponent implements OnInit { + darkMode: boolean; + jobsDataSource: MatTableDataSource = new MatTableDataSource(); + + jobForm: FormGroup; + + constructor( + private eiJobsDataSource: EIJobDataSource, + private ui: UiService + ) { + this.jobForm = new FormGroup({ + id: new FormControl(''), + typeId: new FormControl(''), + owner: new FormControl(''), + targetUri: new FormControl('') + }); + + } + + ngOnInit(): void { + this.refresh(); + + this.jobForm.valueChanges.subscribe(value => { + const filter = { ...value, id: value.id.trim().toLowerCase() } as string; + this.jobsDataSource.filter = filter; + }); + + this.jobsDataSource.filterPredicate = ((data: EIJob, filter) => { + return this.isDataIncluding(data.ei_job_identity, filter.id) + && this.isDataIncluding(data.target_uri, filter.targetUri) + && this.isDataIncluding(data.owner, filter.owner) + && this.isDataIncluding(data.ei_type_identity, filter.typeId); + }) as (data: EIJob, filter: any) => boolean; + + this.ui.darkModeState.subscribe((isDark) => { + this.darkMode = isDark; + }); + } + + sortJobs(sort: Sort) { + const data = this.jobsDataSource.data + data.sort((a: EIJob, b: EIJob) => { + const isAsc = sort.direction === 'asc'; + switch (sort.active) { + case 'id': return this.compare(a.ei_job_identity, b.ei_job_identity, isAsc); + case 'typeId': return this.compare(a.ei_type_identity, b.ei_type_identity, isAsc); + case 'owner': return this.compare(a.owner, b.owner, isAsc); + case 'targetUri': return this.compare(a.target_uri, b.owner, isAsc); + default: return 0; + } + }); + this.jobsDataSource.data = data; + } + + compare(a: any, b: any, isAsc: boolean) { + return (a < b ? -1 : 1) * (isAsc ? 1 : -1); + } + + stopSort(event: any) { + event.stopPropagation(); + } + + isDataIncluding(data: string, filter: string): boolean { + return !filter || data.toLowerCase().includes(filter); + } + + getJobTypeId(eiJob: EIJob): string { + if (eiJob.ei_type_identity) { + return eiJob.ei_type_identity; + } + return '< No type >'; + } + + getJobOwner(eiJob: EIJob): string { + if (eiJob.owner) { + return eiJob.owner; + } + return '< No owner >'; + } + + refresh() { + this.eiJobsDataSource.loadJobs(); + + this.eiJobsDataSource.eiJobsSubject().subscribe((data) => { + this.jobsDataSource.data = data; + }); + } + +} diff --git a/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.html b/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.html index 57e61c5..7e84ea4 100644 --- a/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.html +++ b/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.html @@ -1,50 +1,68 @@ +
- - - -
-
+ + + +
+ Producer ID
-
- {{eiProducer.ei_producer_id}} -
+ + {{eiProducer.ei_producer_id}} + - - -
-
+ + +
+ Producer types -
-
- {{this.getProducerTypes(eiProducer)}} -
+
+
+ {{this.getProducerTypes(eiProducer)}} +
- - -
-
+ + +
+ Producer status -
-
- {{this.getProducerStatus(eiProducer)}} -
+
+
+ {{this.getProducerStatus(eiProducer)}} +
- - -
-
+ + +
+
\ No newline at end of file diff --git a/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.scss b/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.scss index b1963bb..a94c1a5 100644 --- a/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.scss +++ b/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.scss @@ -1,7 +1,30 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2021 Nordix Foundation + * %% + * 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=================================== + */ .ei-coordinator-table { - width: 100%; - min-height: 150px; - margin-top: 10px; - margin-bottom: 10px; - background-color: transparent; - } + width: 100%; + min-height: 150px; + margin-top: 10px; + margin-bottom: 10px; + background-color: transparent; +} +.mat-form-field { + font-size: 14px; + width: 100%; +} \ No newline at end of file diff --git a/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.spec.ts b/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.spec.ts index 2f537f4..9e4860d 100644 --- a/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.spec.ts +++ b/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.spec.ts @@ -1,3 +1,22 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2021 Nordix Foundation + * %% + * 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 { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @@ -7,8 +26,8 @@ import { MatInputHarness } from '@angular/material/input/testing'; import { MatTableModule } from '@angular/material/table'; import { MatTableHarness } from '@angular/material/table/testing'; import { of } from 'rxjs/observable/of'; -import { EIProducer } from 'src/app/interfaces/ei.types'; -import { UiService } from 'src/app/services/ui/ui.service'; +import { EIProducer } from '../../interfaces/ei.types'; +import { UiService } from '../../services/ui/ui.service'; import { EIProducerDataSource } from '../ei-producer.datasource'; import { ProducersListComponent } from './producers-list.component'; diff --git a/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.ts b/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.ts index 8fc57b0..df1d0be 100644 --- a/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.ts +++ b/webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.ts @@ -1,9 +1,28 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2021 Nordix Foundation + * %% + * 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, OnInit } from '@angular/core'; -import { AbstractControl, FormBuilder } from '@angular/forms'; +import { FormControl, FormGroup } from '@angular/forms'; import { Sort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; -import { EIProducer } from 'src/app/interfaces/ei.types'; -import { UiService } from 'src/app/services/ui/ui.service'; +import { EIProducer } from '../../interfaces/ei.types'; +import { UiService } from '../../services/ui/ui.service'; import { EIProducerDataSource } from '../ei-producer.datasource'; @Component({ @@ -14,24 +33,23 @@ import { EIProducerDataSource } from '../ei-producer.datasource'; export class ProducersListComponent implements OnInit { darkMode: boolean; producersDataSource: MatTableDataSource = new MatTableDataSource(); - - readonly producersFormControl: AbstractControl; + producerForm: FormGroup; constructor( private eiProducersDataSource: EIProducerDataSource, - private ui: UiService, - private formBuilder: FormBuilder) { - this.producersFormControl = formBuilder.group({ - ei_producer_id: '', - ei_producer_types: '', - status: '' + private ui: UiService) { + + this.producerForm = new FormGroup({ + ei_producer_id: new FormControl(''), + ei_producer_types: new FormControl(''), + status: new FormControl('') }); } ngOnInit(): void { this.refresh(); - this.producersFormControl.valueChanges.subscribe(value => { + this.producerForm.valueChanges.subscribe(value => { const filter = { ...value, ei_producer_id: value.ei_producer_id.trim().toLowerCase() } as string; this.producersDataSource.filter = filter; }); -- 2.16.6