Create component for EI Producers list 96/5696/2
authorelinuxhenrik <henrik.b.andersson@est.tech>
Thu, 25 Feb 2021 13:32:33 +0000 (14:32 +0100)
committerelinuxhenrik <henrik.b.andersson@est.tech>
Thu, 25 Feb 2021 14:42:02 +0000 (15:42 +0100)
Change-Id: I9e39a731193f1253b2b2a1fc3b5e41fd524c0a3b
Signed-off-by: elinuxhenrik <henrik.b.andersson@est.tech>
Issue-ID: NONRTRIC-455

webapp-frontend/src/app/app.module.ts
webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.html
webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.spec.ts
webapp-frontend/src/app/ei-coordinator/ei-coordinator.component.ts
webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.html [new file with mode: 0644]
webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.scss [new file with mode: 0644]
webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.spec.ts [new file with mode: 0644]
webapp-frontend/src/app/ei-coordinator/producers-list/producers-list.component.ts [new file with mode: 0644]

index be4e434..20fe471 100644 (file)
@@ -68,6 +68,7 @@ import { EICoordinatorComponent } from './ei-coordinator/ei-coordinator.componen
 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;
 
@@ -82,6 +83,7 @@ export const isMock = environment.mock;
     MainComponent,
     NodeModulesComponent,
     SidenavListComponent,
+    ProducersListComponent,
   ],
   imports: [
     AppRoutingModule,
@@ -144,6 +146,7 @@ export const isMock = environment.mock;
   providers: [
     CookieService,
     ErrorDialogService,
+    ProducersListComponent,
     UiService,
     {
       provide: HTTP_INTERCEPTORS,
index fc560dd..57a87f9 100644 (file)
@@ -29,57 +29,7 @@ limitations under the License.
 
 <br>
 <h4>Producers</h4>
-
-<div class="table-container">
-  <mat-table id="producersTable" [dataSource]="producersDataSource" fixedLayout
-    matSort (matSortChange)="sortProducers($event)"
-    class="ei-coordinator-table mat-elevation-z8">
-    <ng-container matColumnDef="id">
-      <mat-header-cell *matHeaderCellDef mat-sort-header>
-        <div (click)="stopSort($event)">
-        <form style="display: flex" [formGroup]="producersFormControl">
-          <mat-form-field>
-            <input id="producerIdFilter" matInput formControlName="ei_producer_id">
-            <mat-placeholder>Producer ID</mat-placeholder>
-          </mat-form-field>
-        </form>
-      </div>
-      </mat-header-cell>
-      <mat-cell *matCellDef="let eiProducer"> {{eiProducer.ei_producer_id}} </mat-cell>
-    </ng-container>
-
-    <ng-container matColumnDef="types">
-      <mat-header-cell *matHeaderCellDef mat-sort-header>
-        <div (click)="stopSort($event)">
-        <form style="display: flex" [formGroup]="producersFormControl">
-          <mat-form-field>
-            <input id="producerTypesFilter" matInput formControlName="ei_producer_types">
-            <mat-placeholder>Producer types</mat-placeholder>
-          </mat-form-field>
-        </form>
-        </div>
-      </mat-header-cell>
-      <mat-cell *matCellDef="let eiProducer"> {{this.getProducerTypes(eiProducer)}} </mat-cell>
-    </ng-container>
-
-    <ng-container matColumnDef="status">
-      <mat-header-cell *matHeaderCellDef mat-sort-header>
-        <div (click)="stopSort($event)">
-        <form style="display: flex" [formGroup]="producersFormControl">
-          <mat-form-field>
-            <input id="producerStatusFilter" matInput formControlName="status">
-            <mat-placeholder>Producer status</mat-placeholder>
-          </mat-form-field>
-        </form>
-        </div>
-      </mat-header-cell>
-      <mat-cell *matCellDef="let eiProducer"> {{this.getProducerStatus(eiProducer)}} </mat-cell>
-    </ng-container>
-
-    <mat-header-row *matHeaderRowDef="['id', 'types', 'status']"></mat-header-row>
-    <mat-row *matRowDef="let row; columns: ['id', 'types', 'status'];"></mat-row>
-  </mat-table>
-</div>
+  <nrcp-producers-list></nrcp-producers-list>
 
 <br>
 <h4>Jobs</h4>
index d516b14..5cda8e5 100644 (file)
@@ -22,37 +22,28 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
 import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
 import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
 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 { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
 import { of } from 'rxjs';
 
 import { EICoordinatorComponent } from './ei-coordinator.component';
 import { EIJobDataSource } from './ei-job.datasource';
-import { EIProducerDataSource } from './ei-producer.datasource';
 import { UiService } from '../services/ui/ui.service';
-import { EIJob, EIProducer } from '../interfaces/ei.types';
+import { EIJob } from '../interfaces/ei.types';
+import { ProducersListComponent } from './producers-list/producers-list.component';
 
 describe('EICoordinatorComponent', () => {
   let component: EICoordinatorComponent;
   let fixture: ComponentFixture<EICoordinatorComponent>;
   let loader: HarnessLoader;
-  let producerDataSourceSpy: jasmine.SpyObj<EIProducerDataSource>;
+  let producersListSpy: jasmine.SpyObj<ProducersListComponent>;
   let jobDataSourceSpy: jasmine.SpyObj<EIJobDataSource>;
 
-  const producer1 = {
-    ei_producer_id: 'producer1',
-    ei_producer_types: [ 'type1', 'type2' ],
-    status: 'ENABLED'
-  } as EIProducer;
-  const producer2 = {
-      ei_producer_id: 'producer2',
-      ei_producer_types: [ 'type2', 'type3' ],
-      status: 'DISABLED'
-  } as EIProducer;
-
   const job1 = {
     ei_job_identity: 'job1',
     ei_type_identity: 'type1',
@@ -67,14 +58,14 @@ describe('EICoordinatorComponent', () => {
   } as EIJob;
 
   beforeEach(async () => {
-    producerDataSourceSpy = jasmine.createSpyObj('EIProducerDataSource', [ 'loadProducers', 'eiProducers', 'eiProducersSubject' ]);
+    producersListSpy = jasmine.createSpyObj('producersListSpy', ['refresh']);
     jobDataSourceSpy = jasmine.createSpyObj('EIJobDataSource', [ 'loadJobs', 'eiJobs', 'eiJobsSubject' ]);
-    
-    producerDataSourceSpy.eiProducersSubject.and.returnValue(of({ producers: [producer1, producer2] }));
-    jobDataSourceSpy.eiJobsSubject.and.returnValue(of({ jobs: [job1, job2] }));
+
+    jobDataSourceSpy.eiJobsSubject.and.returnValue(of([ job1, job2 ]));
 
     await TestBed.configureTestingModule({
       imports: [
+        MatButtonModule,
         MatIconModule,
         MatTableModule,
         BrowserAnimationsModule,
@@ -87,13 +78,13 @@ describe('EICoordinatorComponent', () => {
         EICoordinatorComponent
       ],
       providers: [
+        { provide: ProducersListComponent, useValue: producersListSpy },
         { provide: EIJobDataSource, useValue: jobDataSourceSpy },
-        { provide: EIProducerDataSource, useValue: producerDataSourceSpy },
         UiService,
         FormBuilder,
       ]
     })
-    .compileComponents();
+      .compileComponents();
 
     fixture = TestBed.createComponent(EICoordinatorComponent);
     component = fixture.componentInstance;
@@ -106,26 +97,23 @@ describe('EICoordinatorComponent', () => {
   });
 
   describe('#content', () => {
-    it('should contain refresh button with correct icon', () => {
-      const button = fixture.debugElement.nativeElement.querySelector('#refreshButton');
-      expect(button).toBeTruthy();
-      expect(button.innerHTML).toContain('refresh');
+    it('should contain refresh button with coorect icon', async () => {
+      let refreshButton = await loader.getHarness(MatButtonHarness.with({ selector: '#refreshButton' }));
+      expect(refreshButton).toBeTruthy();
+      expect(await refreshButton.getText()).toEqual('refresh');
     });
 
-    it('should contain producers table with correct columns', async () => {
-      let producersTable = await loader.getHarness(MatTableHarness.with({selector: '#producersTable'}));
-      let headerRow = (await producersTable.getHeaderRows())[0];
-      let headers = await headerRow.getCellTextByColumnName();
-
-      expect(headers).toEqual({id: 'Producer ID', types: 'Producer types', status: 'Producer status'});
+    it('should contain producers table', async () => {
+      const producersTableComponent = fixture.debugElement.nativeElement.querySelector('nrcp-producers-list');
+      expect(producersTableComponent).toBeTruthy();
     });
 
     it('should contain jobs table with correct columns', async () => {
-      let producersTable = await loader.getHarness(MatTableHarness.with({selector: '#jobsTable'}));
+      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'});
+      expect(headers).toEqual({ id: 'Job ID', typeId: 'Type ID', owner: 'Owner', targetUri: 'Target URI' });
     });
 
     it('should set correct dark mode from UIService', () => {
@@ -137,95 +125,19 @@ describe('EICoordinatorComponent', () => {
       expect(component.darkMode).toBeFalsy();
 
     });
-  });
-
-  describe('#producersTable', () => {
-    const expectedProducer1Row = { id: 'producer1', types: 'type1,type2', status: 'ENABLED' };
-    beforeEach(() => {
-      const producers: EIProducer[] =[ producer1, producer2 ];
-      producerDataSourceSpy.eiProducersSubject.and.returnValue(of(producers));
-    });
-
-    it('should contain data after initialization', async () => {
-      component.ngOnInit();
-      const expectedProducerRows = [
-        expectedProducer1Row,
-        {id: 'producer2', types: 'type2,type3', status: 'DISABLED'}
-      ];
-      let producersTable = await loader.getHarness(MatTableHarness.with({selector: '#producersTable'}));
-      let producerRows = await producersTable.getRows();
-      expect(producerRows.length).toEqual(2);
-      producerRows.forEach(row => {
-        row.getCellTextByColumnName().then(values => {
-          expect(expectedProducerRows).toContain(jasmine.objectContaining(values));
-        });
-      });
-    });
-
-    describe('should display default values for non required properties', () => {
-      it('producer defaults', async () => {
-        const producerMissingProperties = {
-          ei_producer_id: 'producer1'
-        } as EIProducer;
-        const producers: EIProducer[] =[ producerMissingProperties ];
-        producerDataSourceSpy.eiProducersSubject.and.returnValue(of(producers));
-        component.ngOnInit();
-
-        const expectedProducerRow = { id: 'producer1', types: '< No types >', status: '< No status >' };
-        let producersTable = await loader.getHarness(MatTableHarness.with({selector: '#producersTable'}));
-        let producerRows = await producersTable.getRows();
-        expect(await producerRows[0].getCellTextByColumnName()).toEqual(expectedProducerRow);
-        });
-
-      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);
-        });
-    });
-
-    it('filtering', async () => {
-      component.ngOnInit();
-      let producersTable = await loader.getHarness(MatTableHarness.with({selector: '#producersTable'}));
 
-      let idFilterInput = await loader.getHarness(MatInputHarness.with({selector: '#producerIdFilter'}));
-      await idFilterInput.setValue("1");
-      let producerRows = await producersTable.getRows();
-      expect(producerRows.length).toEqual(1);
-      expect(await producerRows[0].getCellTextByColumnName()).toEqual(expectedProducer1Row);
-
-      idFilterInput.setValue('');
-      let typesFilterInput = await loader.getHarness(MatInputHarness.with({selector: '#producerTypesFilter'}));
-      await typesFilterInput.setValue("1");
-      producerRows = await producersTable.getRows();
-      expect(producerRows.length).toEqual(1);
-      expect(await producerRows[0].getCellTextByColumnName()).toEqual(expectedProducer1Row);
-      await typesFilterInput.setValue("2");
-      producerRows = await producersTable.getRows();
-      expect(producerRows.length).toEqual(2);
+    it('should refresh tables', async () => {
+      let refreshButton = await loader.getHarness(MatButtonHarness.with({ selector: '#refreshButton' }));
+      await refreshButton.click();
 
-      typesFilterInput.setValue('');
-      let statusFilterInput = await loader.getHarness(MatInputHarness.with({selector: '#producerStatusFilter'}));
-      await statusFilterInput.setValue("enabled");
-      producerRows = await producersTable.getRows();
-      expect(producerRows.length).toEqual(1);
-      expect(await producerRows[0].getCellTextByColumnName()).toEqual(expectedProducer1Row);
+      expect(producersListSpy.refresh).toHaveBeenCalled();
     });
   });
 
   describe('#jobsTable', () => {
     const expectedJob1Row = { id: 'job1', typeId: 'type1', owner: 'owner1', targetUri: 'http://one' };
     beforeEach(() => {
-      const jobs: EIJob[] =[ job1, job2 ];
+      const jobs: EIJob[] = [ job1, job2 ];
       jobDataSourceSpy.eiJobsSubject.and.returnValue(of(jobs));
     });
 
@@ -235,7 +147,7 @@ describe('EICoordinatorComponent', () => {
         expectedJob1Row,
         { id: 'job2', typeId: 'type2', owner: 'owner2', targetUri: 'http://two' }
       ];
-      let jobsTable = await loader.getHarness(MatTableHarness.with({selector: '#jobsTable'}));
+      let jobsTable = await loader.getHarness(MatTableHarness.with({ selector: '#jobsTable' }));
       let jobRows = await jobsTable.getRows();
       expect(jobRows.length).toEqual(2);
       jobRows.forEach(row => {
@@ -245,31 +157,46 @@ describe('EICoordinatorComponent', () => {
       });
     });
 
+    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);
+    });
+
     it('filtering', async () => {
       component.ngOnInit();
-      let jobsTable = await loader.getHarness(MatTableHarness.with({selector: '#jobsTable'}));
+      let jobsTable = await loader.getHarness(MatTableHarness.with({ selector: '#jobsTable' }));
 
-      let idFilterInput = await loader.getHarness(MatInputHarness.with({selector: '#jobIdFilter'}));
+      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'}));
+      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'}));
+      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'}));
+      let targetUriFilterInput = await loader.getHarness(MatInputHarness.with({ selector: '#jobTargetUriFilter' }));
       await targetUriFilterInput.setValue("one");
       jobRows = await jobsTable.getRows();
       expect(jobRows.length).toEqual(1);
index 9698f55..79b7129 100644 (file)
@@ -22,11 +22,10 @@ import { Sort } from '@angular/material/sort';
 import { FormBuilder, FormGroup, AbstractControl } from '@angular/forms';
 import { MatTableDataSource } from '@angular/material/table';
 
-import { EIJob, EIProducer } from '../interfaces/ei.types';
+import { EIJob } from '../interfaces/ei.types';
 import { EIJobDataSource } from './ei-job.datasource';
-import { EIProducerDataSource } from './ei-producer.datasource';
 import { UiService } from '../services/ui/ui.service';
-import { Observable } from 'rxjs/Observable';
+import { ProducersListComponent } from './producers-list/producers-list.component';
 
 @Component({
     selector: 'nrcp-ei-coordinator',
@@ -36,17 +35,14 @@ import { Observable } from 'rxjs/Observable';
 export class EICoordinatorComponent implements OnInit {
 
     darkMode: boolean;
-    searchString: string;
     formGroup: FormGroup;
     jobsDataSource: MatTableDataSource<EIJob> = new MatTableDataSource<EIJob>();
-    producersDataSource: MatTableDataSource<EIProducer>= new MatTableDataSource<EIProducer>();
 
     readonly jobsFormControl: AbstractControl;
-    readonly producersFormControl: AbstractControl;
 
     constructor(
+        private producersList: ProducersListComponent,
         private eiJobsDataSource: EIJobDataSource,
-        private eiProducersDataSource: EIProducerDataSource,
         private ui: UiService,
         private formBuilder: FormBuilder) {
             this.formGroup = formBuilder.group({ filter: [""] });
@@ -57,32 +53,19 @@ export class EICoordinatorComponent implements OnInit {
                 owner: '',
                 targetUri:''
             });
-            this.producersFormControl = formBuilder.group({
-                ei_producer_id: '',
-                ei_producer_types: '',
-                status: ''
-            });
     }
 
     ngOnInit() {
         this.eiJobsDataSource.loadJobs();
-        this.eiProducersDataSource.loadProducers();
 
         this.eiJobsDataSource.eiJobsSubject().subscribe((data) => {
             this.jobsDataSource.data = data;
         });
-        this.eiProducersDataSource.eiProducersSubject().subscribe((data) => {
-            this.producersDataSource.data = data;
-        });
 
         this.jobsFormControl.valueChanges.subscribe(value => {
             const filter = {...value, id: value.id.trim().toLowerCase()} as string;
             this.jobsDataSource.filter = filter;
         });
-        this.producersFormControl.valueChanges.subscribe(value => {
-            const filter = {...value, ei_producer_id: value.ei_producer_id.trim().toLowerCase()} as string;
-            this.producersDataSource.filter = filter;
-        });
 
         this.jobsDataSource.filterPredicate = ((data: EIJob, filter) => {
             return this.isDataIncluding(data.ei_job_identity, filter.id)
@@ -91,12 +74,6 @@ export class EICoordinatorComponent implements OnInit {
                 && this.isDataIncluding(data.ei_type_identity, filter.typeId);
           }) as (data: EIJob, filter: any) => boolean;
 
-        this.producersDataSource.filterPredicate = ((data, filter) => {
-            return this.isDataIncluding(data.ei_producer_id, filter.ei_producer_id)
-                && this.isDataIncluding(data.ei_producer_types.join(','), filter.ei_producer_types)
-                && this.isDataIncluding(data.status, filter.status);
-          }) as (data: EIProducer, filter: any) => boolean;
-
         this.ui.darkModeState.subscribe((isDark) => {
             this.darkMode = isDark;
         });
@@ -117,20 +94,6 @@ export class EICoordinatorComponent implements OnInit {
           this.jobsDataSource.data = data;
     }
 
-    sortProducers(sort: Sort){
-        const data = this.producersDataSource.data
-        data.sort((a: EIProducer, b: EIProducer) => {
-            const isAsc = sort.direction === 'asc';
-            switch (sort.active) {
-              case 'id': return this.compare(a.ei_producer_id, b.ei_producer_id, isAsc);
-              case 'types': return this.compare(a.ei_producer_types, b.ei_producer_types, isAsc);
-              case 'status': return this.compare(a.status, b.status, isAsc);
-              default: return 0;
-            }
-          });
-          this.producersDataSource.data = data;
-    }
-    
     compare(a: any, b: any, isAsc: boolean) {
       return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
     }
@@ -156,30 +119,12 @@ export class EICoordinatorComponent implements OnInit {
         }
         return '< No owner >';
     }
-
-    getProducerTypes(eiProducer: EIProducer): string[] {
-        if (eiProducer.ei_producer_types) {
-            return eiProducer.ei_producer_types;
-        }
-        return ['< No types >'];
-    }
-
-    getProducerStatus(eiProducer: EIProducer): string {
-        if (eiProducer.status) {
-            return eiProducer.status;
-        }
-        return '< No status >';
-    }
-
     refreshTables() {
         this.eiJobsDataSource.loadJobs();
-        this.eiProducersDataSource.loadProducers();
-        
         this.eiJobsDataSource.eiJobsSubject().subscribe((data) => {
             this.jobsDataSource.data = data;
         });
-        this.eiProducersDataSource.eiProducersSubject().subscribe((data) => {
-            this.producersDataSource.data = data;
-        });
+
+        this.producersList.refresh();
     }
 }
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
new file mode 100644 (file)
index 0000000..57e61c5
--- /dev/null
@@ -0,0 +1,50 @@
+<div class="table-container">
+    <mat-table id="producersTable" [dataSource]="producersDataSource" fixedLayout
+      matSort (matSortChange)="sortProducers($event)"
+      class="ei-coordinator-table mat-elevation-z8">
+      <ng-container matColumnDef="id">
+        <mat-header-cell *matHeaderCellDef mat-sort-header>
+          <div (click)="stopSort($event)">
+          <form style="display: flex" [formGroup]="producersFormControl">
+            <mat-form-field>
+              <input id="producerIdFilter" matInput formControlName="ei_producer_id">
+              <mat-placeholder>Producer ID</mat-placeholder>
+            </mat-form-field>
+          </form>
+        </div>
+        </mat-header-cell>
+        <mat-cell *matCellDef="let eiProducer"> {{eiProducer.ei_producer_id}} </mat-cell>
+      </ng-container>
+
+      <ng-container matColumnDef="types">
+        <mat-header-cell *matHeaderCellDef mat-sort-header>
+          <div (click)="stopSort($event)">
+          <form style="display: flex" [formGroup]="producersFormControl">
+            <mat-form-field>
+              <input id="producerTypesFilter" matInput formControlName="ei_producer_types">
+              <mat-placeholder>Producer types</mat-placeholder>
+            </mat-form-field>
+          </form>
+          </div>
+        </mat-header-cell>
+        <mat-cell *matCellDef="let eiProducer"> {{this.getProducerTypes(eiProducer)}} </mat-cell>
+      </ng-container>
+
+      <ng-container matColumnDef="status">
+        <mat-header-cell *matHeaderCellDef mat-sort-header>
+          <div (click)="stopSort($event)">
+          <form style="display: flex" [formGroup]="producersFormControl">
+            <mat-form-field>
+              <input id="producerStatusFilter" matInput formControlName="status">
+              <mat-placeholder>Producer status</mat-placeholder>
+            </mat-form-field>
+          </form>
+          </div>
+        </mat-header-cell>
+        <mat-cell *matCellDef="let eiProducer"> {{this.getProducerStatus(eiProducer)}} </mat-cell>
+      </ng-container>
+
+      <mat-header-row *matHeaderRowDef="['id', 'types', 'status']"></mat-header-row>
+      <mat-row *matRowDef="let row; columns: ['id', 'types', 'status'];"></mat-row>
+    </mat-table>
+  </div>
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
new file mode 100644 (file)
index 0000000..b1963bb
--- /dev/null
@@ -0,0 +1,7 @@
+.ei-coordinator-table {
+    width: 100%;
+    min-height: 150px;
+    margin-top: 10px;
+    margin-bottom: 10px;
+    background-color: transparent;
+  }
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
new file mode 100644 (file)
index 0000000..2f537f4
--- /dev/null
@@ -0,0 +1,140 @@
+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 { FormBuilder, ReactiveFormsModule } 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 { EIProducer } from 'src/app/interfaces/ei.types';
+import { UiService } from 'src/app/services/ui/ui.service';
+import { EIProducerDataSource } from '../ei-producer.datasource';
+
+import { ProducersListComponent } from './producers-list.component';
+
+describe('ProducersListComponent', () => {
+  let component: ProducersListComponent;
+  let fixture: ComponentFixture<ProducersListComponent>;
+  let loader: HarnessLoader;
+  let producerDataSourceSpy: jasmine.SpyObj<EIProducerDataSource>;
+
+  const producer1 = {
+    ei_producer_id: 'producer1',
+    ei_producer_types: ['type1', 'type2'],
+    status: 'ENABLED'
+  } as EIProducer;
+  const producer2 = {
+    ei_producer_id: 'producer2',
+    ei_producer_types: ['type2', 'type3'],
+    status: 'DISABLED'
+  } as EIProducer;
+
+  beforeEach(async(() => {
+    producerDataSourceSpy = jasmine.createSpyObj('EIProducerDataSource', ['loadProducers', 'eiProducers', 'eiProducersSubject']);
+
+    const producers: EIProducer[] = [producer1, producer2];
+    producerDataSourceSpy.eiProducersSubject.and.returnValue(of(producers));
+
+    TestBed.configureTestingModule({
+      imports: [
+        MatTableModule,
+        ReactiveFormsModule
+      ],
+      schemas: [
+        CUSTOM_ELEMENTS_SCHEMA
+      ],
+      declarations: [
+        ProducersListComponent
+      ],
+      providers: [
+        { provide: EIProducerDataSource, useValue: producerDataSourceSpy },
+        UiService,
+        FormBuilder,
+      ]
+    })
+      .compileComponents();
+  }));
+
+  const expectedProducer1Row = { id: 'producer1', types: 'type1,type2', status: 'ENABLED' };
+  beforeEach(() => {
+    fixture = TestBed.createComponent(ProducersListComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+    loader = TestbedHarnessEnvironment.loader(fixture);
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should contain producers table with correct columns', async () => {
+    let producersTable = await loader.getHarness(MatTableHarness.with({ selector: '#producersTable' }));
+    let headerRow = (await producersTable.getHeaderRows())[0];
+    let headers = await headerRow.getCellTextByColumnName();
+
+    expect(headers).toEqual({ id: 'Producer ID', types: 'Producer types', status: 'Producer status' });
+  });
+
+
+  it('should contain data after initialization', async () => {
+    component.ngOnInit();
+    const expectedProducerRows = [
+      expectedProducer1Row,
+      { id: 'producer2', types: 'type2,type3', status: 'DISABLED' }
+    ];
+    let producersTable = await loader.getHarness(MatTableHarness.with({ selector: '#producersTable' }));
+    let producerRows = await producersTable.getRows();
+    expect(producerRows.length).toEqual(2);
+    producerRows.forEach(row => {
+      row.getCellTextByColumnName().then(values => {
+        expect(expectedProducerRows).toContain(jasmine.objectContaining(values));
+      });
+    });
+  });
+
+  describe('should display default values for non required properties', () => {
+    it('producer defaults', async () => {
+      const producerMissingProperties = {
+        ei_producer_id: 'producer1'
+      } as EIProducer;
+      const producers: EIProducer[] = [producerMissingProperties];
+      producerDataSourceSpy.eiProducersSubject.and.returnValue(of(producers));
+      component.ngOnInit();
+
+      const expectedProducerRow = { id: 'producer1', types: '< No types >', status: '< No status >' };
+      let producersTable = await loader.getHarness(MatTableHarness.with({ selector: '#producersTable' }));
+      let producerRows = await producersTable.getRows();
+      expect(await producerRows[0].getCellTextByColumnName()).toEqual(expectedProducerRow);
+    });
+  });
+
+  it('filtering', async () => {
+    const expectedProducer1Row = { id: 'producer1', types: 'type1,type2', status: 'ENABLED' };
+    component.ngOnInit();
+    let producersTable = await loader.getHarness(MatTableHarness.with({ selector: '#producersTable' }));
+
+    let idFilterInput = await loader.getHarness(MatInputHarness.with({ selector: '#producerIdFilter' }));
+    await idFilterInput.setValue("1");
+    let producerRows = await producersTable.getRows();
+    expect(producerRows.length).toEqual(1);
+    expect(await producerRows[0].getCellTextByColumnName()).toEqual(expectedProducer1Row);
+
+    idFilterInput.setValue('');
+    let typesFilterInput = await loader.getHarness(MatInputHarness.with({ selector: '#producerTypesFilter' }));
+    await typesFilterInput.setValue("1");
+    producerRows = await producersTable.getRows();
+    expect(producerRows.length).toEqual(1);
+    expect(await producerRows[0].getCellTextByColumnName()).toEqual(expectedProducer1Row);
+    await typesFilterInput.setValue("2");
+    producerRows = await producersTable.getRows();
+    expect(producerRows.length).toEqual(2);
+
+    typesFilterInput.setValue('');
+    let statusFilterInput = await loader.getHarness(MatInputHarness.with({ selector: '#producerStatusFilter' }));
+    await statusFilterInput.setValue("enabled");
+    producerRows = await producersTable.getRows();
+    expect(producerRows.length).toEqual(1);
+    expect(await producerRows[0].getCellTextByColumnName()).toEqual(expectedProducer1Row);
+  });
+});
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
new file mode 100644 (file)
index 0000000..8fc57b0
--- /dev/null
@@ -0,0 +1,96 @@
+import { Component, OnInit } from '@angular/core';
+import { AbstractControl, FormBuilder } 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 { EIProducerDataSource } from '../ei-producer.datasource';
+
+@Component({
+  selector: 'nrcp-producers-list',
+  templateUrl: './producers-list.component.html',
+  styleUrls: ['./producers-list.component.scss']
+})
+export class ProducersListComponent implements OnInit {
+  darkMode: boolean;
+  producersDataSource: MatTableDataSource<EIProducer> = new MatTableDataSource<EIProducer>();
+
+  readonly producersFormControl: AbstractControl;
+
+  constructor(
+    private eiProducersDataSource: EIProducerDataSource,
+    private ui: UiService,
+    private formBuilder: FormBuilder) {
+    this.producersFormControl = formBuilder.group({
+      ei_producer_id: '',
+      ei_producer_types: '',
+      status: ''
+    });
+  }
+
+  ngOnInit(): void {
+    this.refresh();
+
+    this.producersFormControl.valueChanges.subscribe(value => {
+      const filter = { ...value, ei_producer_id: value.ei_producer_id.trim().toLowerCase() } as string;
+      this.producersDataSource.filter = filter;
+    });
+
+    this.producersDataSource.filterPredicate = ((data, filter) => {
+      return this.isDataIncluding(data.ei_producer_id, filter.ei_producer_id)
+        && this.isDataIncluding(data.ei_producer_types.join(','), filter.ei_producer_types)
+        && this.isDataIncluding(data.status, filter.status);
+    }) as (data: EIProducer, filter: any) => boolean;
+
+    this.ui.darkModeState.subscribe((isDark) => {
+      this.darkMode = isDark;
+    });
+  }
+
+  isDataIncluding(data: string, filter: string): boolean {
+    return !filter || data.toLowerCase().includes(filter);
+  }
+
+  sortProducers(sort: Sort) {
+    const data = this.producersDataSource.data
+    data.sort((a: EIProducer, b: EIProducer) => {
+      const isAsc = sort.direction === 'asc';
+      switch (sort.active) {
+        case 'id': return this.compare(a.ei_producer_id, b.ei_producer_id, isAsc);
+        case 'types': return this.compare(a.ei_producer_types, b.ei_producer_types, isAsc);
+        case 'status': return this.compare(a.status, b.status, isAsc);
+        default: return 0;
+      }
+    });
+    this.producersDataSource.data = data;
+  }
+
+  compare(a: any, b: any, isAsc: boolean) {
+    return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
+  }
+
+  stopSort(event: any) {
+    event.stopPropagation();
+  }
+
+  getProducerTypes(eiProducer: EIProducer): string[] {
+    if (eiProducer.ei_producer_types) {
+      return eiProducer.ei_producer_types;
+    }
+    return ['< No types >'];
+  }
+
+  getProducerStatus(eiProducer: EIProducer): string {
+    if (eiProducer.status) {
+      return eiProducer.status;
+    }
+    return '< No status >';
+  }
+
+  refresh() {
+    this.eiProducersDataSource.loadProducers();
+    this.eiProducersDataSource.eiProducersSubject().subscribe((data) => {
+      this.producersDataSource.data = data;
+    });
+  }
+}