[CST-18016] Create a new component to list the qa sources

This commit is contained in:
Giuseppe Digilio
2021-01-22 10:25:06 +01:00
parent 20df163444
commit 117514bace
8 changed files with 305 additions and 73 deletions

View File

@@ -5,65 +5,12 @@
<ds-alert [type]="'alert-info'" [content]="'quality-assurance.source.description'"></ds-alert>
</div>
</div>
<div class="row">
<div class="col-12">
<h2 class="h4 border-bottom pb-2">{{'quality-assurance.source'| translate}}</h2>
@if ((isSourceLoading() | async)) {
<ds-loading class="container" message="{{'quality-assurance.loading' | translate}}"></ds-loading>
}
@if ((isSourceLoading() | async) !== true) {
<ds-pagination
[paginationOptions]="paginationConfig"
[collectionSize]="(totalElements$ | async)"
[hideGear]="false"
[hideSortOptions]="true"
(paginationChange)="getQualityAssuranceSource()">
@if ((isSourceProcessing() | async)) {
<ds-loading class="container" message="'quality-assurance.loading' | translate"></ds-loading>
}
@if ((isSourceProcessing() | async) !== true) {
@if ((sources$ | async)?.length === 0) {
<div class="alert alert-info w-100 mb-2 mt-2" role="alert">
{{'quality-assurance.noSource' | translate}}
</div>
}
@if ((sources$ | async)?.length !== 0) {
<div class="table-responsive mt-2">
<table id="epeople" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col">{{'quality-assurance.table.source' | translate}}</th>
<th scope="col">{{'quality-assurance.table.last-event' | translate}}</th>
<th scope="col">{{'quality-assurance.table.actions' | translate}}</th>
</tr>
</thead>
<tbody>
@for (sourceElement of (sources$ | async); track sourceElement; let i = $index) {
<tr>
<td>{{sourceElement.id}}</td>
<td>{{sourceElement.lastEvent | date: 'dd/MM/yyyy hh:mm' }}</td>
<td>
<div class="btn-group edit-field">
<button
class="btn btn-outline-primary btn-sm"
title="{{'quality-assurance.source-list.button.detail' | translate : { param: sourceElement.id } }}"
[routerLink]="[sourceElement.id]">
<span class="badge bg-info">{{sourceElement.totalEvents}}</span>
<i class="fas fa-info fa-fw"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
}
</ds-pagination>
}
</div>
</div>
<ds-source-list [loading]="(isSourceLoading() | async) === true || (isSourceProcessing() | async) === true"
[paginationConfig]="paginationConfig"
[showLastEvent]="true"
[sources]="sources$ | async"
[totalElements]="totalElements$ | async"
(paginationChange)="getQualityAssuranceSource()"
(sourceSelected)="onSelect($event)"></ds-source-list>
</div>

View File

@@ -16,16 +16,18 @@ import { of as observableOf } from 'rxjs';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { AlertComponent } from '../../../shared/alert/alert.component';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
import {
getMockNotificationsStateService,
qualityAssuranceSourceObjectMoreAbstract,
qualityAssuranceSourceObjectMorePid,
} from '../../../shared/mocks/notifications.mock';
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { createTestComponent } from '../../../shared/testing/utils.test';
import { NotificationsStateService } from '../../notifications-state.service';
import {
SourceListComponent,
SourceObject,
} from '../../shared/source-list.component';
import { QualityAssuranceSourceComponent } from './quality-assurance-source.component';
describe('QualityAssuranceSourceComponent test suite', () => {
@@ -61,8 +63,7 @@ describe('QualityAssuranceSourceComponent test suite', () => {
remove: {
imports: [
AlertComponent,
ThemedLoadingComponent,
PaginationComponent,
SourceListComponent,
],
},
})
@@ -119,12 +120,19 @@ describe('QualityAssuranceSourceComponent test suite', () => {
it(('Should init component properly'), () => {
comp.ngOnInit();
fixture.detectChanges();
const expected: SourceObject[] = [
{ id: qualityAssuranceSourceObjectMorePid.id,
lastEvent: qualityAssuranceSourceObjectMorePid.lastEvent,
total: qualityAssuranceSourceObjectMorePid.totalEvents,
},
{ id: qualityAssuranceSourceObjectMoreAbstract.id,
lastEvent: qualityAssuranceSourceObjectMoreAbstract.lastEvent,
total: qualityAssuranceSourceObjectMoreAbstract.totalEvents,
},
];
expect(comp.sources$).toBeObservable(cold('(a|)', {
a: [
qualityAssuranceSourceObjectMorePid,
qualityAssuranceSourceObjectMoreAbstract,
],
a: expected,
}));
expect(comp.totalElements$).toBeObservable(cold('(a|)', {
a: 2,

View File

@@ -8,7 +8,11 @@ import {
OnDestroy,
OnInit,
} from '@angular/core';
import { RouterLink } from '@angular/router';
import {
ActivatedRoute,
Router,
RouterLink,
} from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import {
Observable,
@@ -16,6 +20,7 @@ import {
} from 'rxjs';
import {
distinctUntilChanged,
map,
take,
} from 'rxjs/operators';
@@ -29,6 +34,10 @@ import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.c
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { NotificationsStateService } from '../../notifications-state.service';
import {
SourceListComponent,
SourceObject,
} from '../../shared/source-list.component';
/**
* Component to display the Quality Assurance source list.
@@ -38,7 +47,7 @@ import { NotificationsStateService } from '../../notifications-state.service';
templateUrl: './quality-assurance-source.component.html',
styleUrls: ['./quality-assurance-source.component.scss'],
standalone: true,
imports: [AlertComponent, ThemedLoadingComponent, PaginationComponent, RouterLink, AsyncPipe, TranslateModule, DatePipe],
imports: [AlertComponent, ThemedLoadingComponent, PaginationComponent, RouterLink, AsyncPipe, TranslateModule, DatePipe, SourceListComponent],
})
export class QualityAssuranceSourceComponent implements OnDestroy, OnInit, AfterViewInit {
@@ -59,7 +68,7 @@ export class QualityAssuranceSourceComponent implements OnDestroy, OnInit, After
/**
* The Quality Assurance source list.
*/
public sources$: Observable<QualityAssuranceSourceObject[]>;
public sources$: Observable<SourceObject[]>;
/**
* The total number of Quality Assurance sources.
*/
@@ -74,10 +83,14 @@ export class QualityAssuranceSourceComponent implements OnDestroy, OnInit, After
* Initialize the component variables.
* @param {PaginationService} paginationService
* @param {NotificationsStateService} notificationsStateService
* @param {Router} router
* @param {ActivatedRoute} route
*/
constructor(
private paginationService: PaginationService,
private notificationsStateService: NotificationsStateService,
private router: Router,
private route: ActivatedRoute,
) {
}
@@ -85,7 +98,15 @@ export class QualityAssuranceSourceComponent implements OnDestroy, OnInit, After
* Component initialization.
*/
ngOnInit(): void {
this.sources$ = this.notificationsStateService.getQualityAssuranceSource();
this.sources$ = this.notificationsStateService.getQualityAssuranceSource().pipe(
map((sources: QualityAssuranceSourceObject[])=> {
return sources.map((source: QualityAssuranceSourceObject) => ({
id: source.id,
lastEvent: source.lastEvent,
total: source.totalEvents,
}));
}),
);
this.totalElements$ = this.notificationsStateService.getQualityAssuranceSourceTotals();
}
@@ -102,6 +123,14 @@ export class QualityAssuranceSourceComponent implements OnDestroy, OnInit, After
);
}
/**
* Navigate to the specified source
* @param sourceId
*/
onSelect(sourceId: string) {
this.router.navigate([sourceId], { relativeTo: this.route });
}
/**
* Returns the information about the loading status of the Quality Assurance source (if it's running or not).
*

View File

@@ -0,0 +1,59 @@
<div class="col-12">
<h2 class="h4 border-bottom pb-2">{{'quality-assurance.source'| translate}}</h2>
@if (loading()) {
<ds-loading class="container" message="{{'quality-assurance.loading' | translate}}"></ds-loading>
} @else {
<ds-pagination
[paginationOptions]="paginationConfig()"
[collectionSize]="totalElements()"
[hideGear]="false"
[hideSortOptions]="true"
(paginationChange)="paginationChange.emit($event)">
@if (sources()?.length === 0) {
<div class="alert alert-info w-100 mb-2 mt-2" role="alert">
{{'quality-assurance.noSource' | translate}}
</div>
}
@if (sources()?.length !== 0) {
<div class="table-responsive mt-2">
<table id="epeople" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col">{{'quality-assurance.table.source' | translate}}</th>
@if (showLastEvent()) {
<th scope="col">{{'quality-assurance.table.last-event' | translate}}</th>
}
<th scope="col">{{'quality-assurance.table.actions' | translate}}</th>
</tr>
</thead>
<tbody>
@for (sourceElement of sources(); track sourceElement; let i = $index) {
<tr>
<td>{{sourceElement.id}}</td>
@if (showLastEvent()) {
<td>{{sourceElement.lastEvent | date: 'dd/MM/yyyy hh:mm' }}</td>
}
<td>
<div class="btn-group edit-field">
<button
class="btn btn-outline-primary btn-sm"
title="{{'quality-assurance.source-list.button.detail' | translate : { param: sourceElement.id } }}"
(click)="sourceSelected.emit(sourceElement.id)">
<span class="badge bg-info">{{sourceElement.total}}</span>
<i class="fas fa-info fa-fw"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</ds-pagination>
}
</div>

View File

@@ -0,0 +1,114 @@
import {
AsyncPipe,
DatePipe,
} from '@angular/common';
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { AlertComponent } from '../../shared/alert/alert.component';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { PaginationComponent } from '../../shared/pagination/pagination.component';
import {
SourceListComponent,
SourceObject,
} from './source-list.component';
describe('SourceListComponent', () => {
let component: SourceListComponent;
let fixture: ComponentFixture<SourceListComponent>;
const paginationConfig = {
currentPage: 1,
pageSize: 10,
};
const sources: SourceObject[] = [
{ id: 'source1', lastEvent: '2025-03-12T12:00:00', total: 5 },
{ id: 'source1', lastEvent: '2025-03-13T12:00:00', total: 10 },
];
const sourcesWithoutEvent: SourceObject[] = [
{ id: 'source1', total: 5 },
{ id: 'source1', total: 10 },
];
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SourceListComponent],
imports: [
TranslateModule.forRoot(),
AlertComponent,
AsyncPipe,
DatePipe,
MockComponent(PaginationComponent),
MockComponent(ThemedLoadingComponent),
],
providers: [
provideRouter([]),
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SourceListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display loading message when loading is true', () => {
fixture.componentRef.setInput('loading', true);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('ds-loading')).toBeTruthy();
});
it('should display sources when loading is false and sources are available', () => {
fixture.componentRef.setInput('loading', false);
fixture.componentRef.setInput('showLastEvent', true);
fixture.componentRef.setInput('sources', sources);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('table')).toBeTruthy();
expect(fixture.nativeElement.querySelector('thead tr').children.length).toBe(3);
expect(fixture.nativeElement.querySelector('tbody').children.length).toBe(2);
expect(fixture.nativeElement.querySelector('tbody tr td').textContent).toContain('source1');
});
it('should not display last event column', () => {
fixture.componentRef.setInput('loading', false);
fixture.componentRef.setInput('showLastEvent', false);
fixture.componentRef.setInput('sources', sourcesWithoutEvent);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('table')).toBeTruthy();
expect(fixture.nativeElement.querySelector('thead tr').children.length).toBe(2);
expect(fixture.nativeElement.querySelector('tbody').children.length).toBe(2);
expect(fixture.nativeElement.querySelector('table tbody tr td').textContent).toContain('source1');
});
it('should emit sourceSelected event when a source is clicked', () => {
spyOn(component.sourceSelected, 'emit');
fixture.componentRef.setInput('loading', false);
fixture.componentRef.setInput('paginationConfig', paginationConfig);
fixture.componentRef.setInput('sources', sources);
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('.btn-outline-primary');
button.click();
expect(component.sourceSelected.emit).toHaveBeenCalledWith('source1');
});
it('should emit paginationChange event when pagination changes', () => {
spyOn(component.paginationChange, 'emit');
fixture.componentRef.setInput('loading', false);
fixture.componentRef.setInput('paginationConfig', paginationConfig);
fixture.detectChanges();
const paginationComponent = fixture.nativeElement.querySelector('ds-pagination');
paginationComponent.dispatchEvent(new Event('paginationChange'));
expect(component.paginationChange.emit).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,75 @@
import {
AsyncPipe,
DatePipe,
} from '@angular/common';
import {
Component,
input,
InputSignal,
output,
} from '@angular/core';
import { RouterLink } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { AlertComponent } from '../../shared/alert/alert.component';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { PaginationComponent } from '../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
export interface SourceObject {
id: string;
lastEvent?: string;
total: number;
}
/**
* Component to display the Quality Assurance source list.
*/
@Component({
selector: 'ds-source-list',
templateUrl: './source-list.component.html',
styleUrls: ['./source-list.component.scss'],
standalone: true,
imports: [AlertComponent, ThemedLoadingComponent, PaginationComponent, RouterLink, AsyncPipe, TranslateModule, DatePipe],
})
export class SourceListComponent {
/**
* A boolean indicating whether the sources are in a loading state.
*/
loading: InputSignal<boolean> = input<boolean>(false);
/**
* The pagination system configuration for HTML listing.
* @type {PaginationComponentOptions}
*/
paginationConfig: InputSignal<PaginationComponentOptions> = input<PaginationComponentOptions>();
/**
* A boolean indicating whether to show the last event column.
*/
showLastEvent: InputSignal<boolean> = input<boolean>();
/**
* The source list.
*/
sources: InputSignal<SourceObject[]|null> = input<SourceObject[]|null>();
/**
* The total number of Quality Assurance sources.
*/
totalElements: InputSignal<number> = input<number>();
/**
* Event emitter for when a source is selected.
* Emits the ID of the selected source.
*/
sourceSelected = output<string>();
/**
* Event emitter for when the pagination changes.
* Emits a string representation of the pagination change.
*/
paginationChange = output<string>();
}

View File

@@ -34,7 +34,7 @@ export const qualityAssuranceSourceDataResolver: ResolveFn<QualityAssuranceSourc
): Observable<QualityAssuranceSourceObject[]> => {
const pageSize = appConfig.qualityAssuranceConfig.pageSize;
return qualityAssuranceSourceService.getSources(pageSize, 0).pipe(
return qualityAssuranceSourceService.getSources(pageSize, 1).pipe(
map((sources: PaginatedList<QualityAssuranceSourceObject>) => {
if (sources.page.length === 1) {
router.navigate([getResolvedUrl(route) + '/' + sources.page[0].id]);