108915: Refactored code to use followLinks to retrieve the files of a process instead of a second request

This commit is contained in:
Alexandre Vryghem
2023-11-30 17:30:22 +01:00
parent 9b5001e1d9
commit 6b0f2e7c44
5 changed files with 89 additions and 77 deletions

View File

@@ -12,7 +12,7 @@ import { Bitstream } from '../../shared/bitstream.model';
import { RemoteData } from '../remote-data'; import { RemoteData } from '../remote-data';
import { BitstreamDataService } from '../bitstream-data.service'; import { BitstreamDataService } from '../bitstream-data.service';
import { IdentifiableDataService } from '../base/identifiable-data.service'; import { IdentifiableDataService } from '../base/identifiable-data.service';
import { FollowLinkConfig, followLink } from '../../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { FindAllData, FindAllDataImpl } from '../base/find-all-data'; import { FindAllData, FindAllDataImpl } from '../base/find-all-data';
import { FindListOptions } from '../find-list-options.model'; import { FindListOptions } from '../find-list-options.model';
import { dataService } from '../base/data-service.decorator'; import { dataService } from '../base/data-service.decorator';
@@ -35,21 +35,6 @@ export const TIMER_FACTORY = new InjectionToken<(callback: (...args: any[]) => v
@Injectable() @Injectable()
@dataService(PROCESS) @dataService(PROCESS)
export class ProcessDataService extends IdentifiableDataService<Process> implements FindAllData<Process>, DeleteData<Process> { export class ProcessDataService extends IdentifiableDataService<Process> implements FindAllData<Process>, DeleteData<Process> {
/**
* Return true if the given process has the given status
* @protected
*/
protected static statusIs(process: Process, status: ProcessStatus): boolean {
return hasValue(process) && process.processStatus === status;
}
/**
* Return true if the given process has the status COMPLETED or FAILED
*/
public static hasCompletedOrFailed(process: Process): boolean {
return ProcessDataService.statusIs(process, ProcessStatus.COMPLETED) ||
ProcessDataService.statusIs(process, ProcessStatus.FAILED);
}
private findAllData: FindAllData<Process>; private findAllData: FindAllData<Process>;
private deleteData: DeleteData<Process>; private deleteData: DeleteData<Process>;
@@ -71,6 +56,22 @@ export class ProcessDataService extends IdentifiableDataService<Process> impleme
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
} }
/**
* Return true if the given process has the given status
* @protected
*/
protected static statusIs(process: Process, status: ProcessStatus): boolean {
return hasValue(process) && process.processStatus === status;
}
/**
* Return true if the given process has the status COMPLETED or FAILED
*/
public static hasCompletedOrFailed(process: Process): boolean {
return ProcessDataService.statusIs(process, ProcessStatus.COMPLETED) ||
ProcessDataService.statusIs(process, ProcessStatus.FAILED);
}
/** /**
* Get the endpoint for the files of the process * Get the endpoint for the files of the process
* @param processId The ID of the process * @param processId The ID of the process
@@ -153,11 +154,14 @@ export class ProcessDataService extends IdentifiableDataService<Process> impleme
* status. That makes it more convenient to retrieve that process for a component: you can replace * status. That makes it more convenient to retrieve that process for a component: you can replace
* a findByID call with this method, rather than having to do a separate findById, and then call * a findByID call with this method, rather than having to do a separate findById, and then call
* this method * this method
* @param processId *
* @param pollingIntervalInMs * @param processId The ID of the {@link Process} to poll
* @param pollingIntervalInMs The interval for how often the request needs to be polled
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be
* automatically resolved
*/ */
public autoRefreshUntilCompletion(processId: string, pollingIntervalInMs = 5000): Observable<RemoteData<Process>> { public autoRefreshUntilCompletion(processId: string, pollingIntervalInMs = 5000, ...linksToFollow: FollowLinkConfig<Process>[]): Observable<RemoteData<Process>> {
const process$ = this.findById(processId, true, true, followLink('script')) const process$: Observable<RemoteData<Process>> = this.findById(processId, true, true, ...linksToFollow)
.pipe( .pipe(
getAllCompletedRemoteData(), getAllCompletedRemoteData(),
); );

View File

@@ -27,7 +27,6 @@ import { ProcessDataService } from '../../core/data/processes/process-data.servi
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { import {
createFailedRemoteDataObject$, createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$ createSuccessfulRemoteDataObject$
} from '../../shared/remote-data.utils'; } from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
@@ -35,6 +34,10 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { getProcessListRoute } from '../process-page-routing.paths'; import { getProcessListRoute } from '../process-page-routing.paths';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { RouterTestingModule } from '@angular/router/testing';
import { RouterStub } from '../../shared/testing/router.stub';
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
describe('ProcessDetailComponent', () => { describe('ProcessDetailComponent', () => {
let component: ProcessDetailComponent; let component: ProcessDetailComponent;
@@ -44,44 +47,18 @@ describe('ProcessDetailComponent', () => {
let nameService: DSONameService; let nameService: DSONameService;
let bitstreamDataService: BitstreamDataService; let bitstreamDataService: BitstreamDataService;
let httpClient: HttpClient; let httpClient: HttpClient;
let route: ActivatedRoute; let route: ActivatedRouteStub;
let router: RouterStub;
let modalService;
let notificationsService: NotificationsServiceStub;
let process: Process; let process: Process;
let fileName: string; let fileName: string;
let files: Bitstream[]; let files: Bitstream[];
let processOutput; let processOutput: string;
let modalService;
let notificationsService;
let router;
function init() { function init() {
processOutput = 'Process Started';
process = Object.assign(new Process(), {
processId: 1,
scriptName: 'script-name',
processStatus: 'COMPLETED',
parameters: [
{
name: '-f',
value: 'file.xml'
},
{
name: '-i',
value: 'identifier'
}
],
_links: {
self: {
href: 'https://rest.api/processes/1'
},
output: {
href: 'https://rest.api/processes/1/output'
}
}
});
fileName = 'fake-file-name'; fileName = 'fake-file-name';
files = [ files = [
Object.assign(new Bitstream(), { Object.assign(new Bitstream(), {
@@ -99,6 +76,33 @@ describe('ProcessDetailComponent', () => {
} }
}) })
]; ];
processOutput = 'Process Started';
process = Object.assign(new Process(), {
processId: 1,
scriptName: 'script-name',
processStatus: 'COMPLETED',
parameters: [
{
name: '-f',
value: 'file.xml'
},
{
name: '-i',
value: 'identifier'
}
],
files: createSuccessfulRemoteDataObject$(Object.assign(new PaginatedList(), {
page: files,
})),
_links: {
self: {
href: 'https://rest.api/processes/1'
},
output: {
href: 'https://rest.api/processes/1/output'
}
},
});
const logBitstream = Object.assign(new Bitstream(), { const logBitstream = Object.assign(new Bitstream(), {
id: 'output.log', id: 'output.log',
_links: { _links: {
@@ -127,33 +131,22 @@ describe('ProcessDetailComponent', () => {
notificationsService = new NotificationsServiceStub(); notificationsService = new NotificationsServiceStub();
router = jasmine.createSpyObj('router', { router = new RouterStub();
navigateByUrl:{}
});
route = jasmine.createSpyObj('route', { route = new ActivatedRouteStub({
data: observableOf({ process: createSuccessfulRemoteDataObject$(process) }), id: process.processId,
snapshot: { }, {
params: { id: process.processId } process: createSuccessfulRemoteDataObject$(process),
}
}); });
} }
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
init(); init();
TestBed.configureTestingModule({ void TestBed.configureTestingModule({
declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe], declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe],
imports: [TranslateModule.forRoot()], imports: [TranslateModule.forRoot(), RouterTestingModule],
providers: [ providers: [
{ { provide: ActivatedRoute, useValue: route },
provide: ActivatedRoute,
useValue: {
data: observableOf({ process: createSuccessfulRemoteDataObject(process) }),
snapshot: {
params: { id: process.processId }
}
}
},
{ provide: ProcessDataService, useValue: processService }, { provide: ProcessDataService, useValue: processService },
{ provide: BitstreamDataService, useValue: bitstreamDataService }, { provide: BitstreamDataService, useValue: bitstreamDataService },
{ provide: DSONameService, useValue: nameService }, { provide: DSONameService, useValue: nameService },
@@ -258,6 +251,8 @@ describe('ProcessDetailComponent', () => {
describe('deleteProcess', () => { describe('deleteProcess', () => {
it('should delete the process and navigate back to the overview page on success', () => { it('should delete the process and navigate back to the overview page on success', () => {
spyOn(component, 'closeModal'); spyOn(component, 'closeModal');
spyOn(router, 'navigateByUrl').and.callThrough();
component.deleteProcess(process); component.deleteProcess(process);
expect(processService.delete).toHaveBeenCalledWith(process.processId); expect(processService.delete).toHaveBeenCalledWith(process.processId);
@@ -268,6 +263,7 @@ describe('ProcessDetailComponent', () => {
it('should delete the process and not navigate on error', () => { it('should delete the process and not navigate on error', () => {
(processService.delete as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$()); (processService.delete as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
spyOn(component, 'closeModal'); spyOn(component, 'closeModal');
spyOn(router, 'navigateByUrl').and.callThrough();
component.deleteProcess(process); component.deleteProcess(process);

View File

@@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Component, Inject, NgZone, OnInit, PLATFORM_ID } from '@angular/core'; import { Component, Inject, NgZone, OnInit, PLATFORM_ID } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { finalize, map, switchMap, take, tap, find, startWith } from 'rxjs/operators'; import { finalize, map, switchMap, take, tap, find, startWith } from 'rxjs/operators';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
@@ -27,6 +27,7 @@ import { getProcessListRoute } from '../process-page-routing.paths';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
import { PROCESS_PAGE_FOLLOW_LINKS } from '../process-page.resolver';
@Component({ @Component({
selector: 'ds-process-detail', selector: 'ds-process-detail',
@@ -84,8 +85,6 @@ export class ProcessDetailComponent implements OnInit {
*/ */
protected modalRef: NgbModalRef; protected modalRef: NgbModalRef;
private refreshTimerSub?: Subscription;
constructor( constructor(
@Inject(PLATFORM_ID) protected platformId: object, @Inject(PLATFORM_ID) protected platformId: object,
protected route: ActivatedRoute, protected route: ActivatedRoute,
@@ -109,7 +108,7 @@ export class ProcessDetailComponent implements OnInit {
this.processRD$ = this.route.data.pipe( this.processRD$ = this.route.data.pipe(
switchMap((data) => { switchMap((data) => {
if (isPlatformBrowser(this.platformId)) { if (isPlatformBrowser(this.platformId)) {
return this.processService.autoRefreshUntilCompletion(this.route.snapshot.params.id, 5000); return this.processService.autoRefreshUntilCompletion(this.route.snapshot.params.id, 5000, ...PROCESS_PAGE_FOLLOW_LINKS);
} else { } else {
return [data.process as RemoteData<Process>]; return [data.process as RemoteData<Process>];
} }
@@ -125,7 +124,7 @@ export class ProcessDetailComponent implements OnInit {
this.filesRD$ = this.processRD$.pipe( this.filesRD$ = this.processRD$.pipe(
getAllSucceededRemoteDataPayload(), getAllSucceededRemoteDataPayload(),
switchMap((process: Process) => this.processService.getFiles(process.processId)) switchMap((process: Process) => process.files),
); );
} }

View File

@@ -7,6 +7,10 @@ import { followLink } from '../shared/utils/follow-link-config.model';
import { ProcessDataService } from '../core/data/processes/process-data.service'; import { ProcessDataService } from '../core/data/processes/process-data.service';
import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { getFirstCompletedRemoteData } from '../core/shared/operators';
export const PROCESS_PAGE_FOLLOW_LINKS = [
followLink('files'),
];
/** /**
* This class represents a resolver that requests a specific process before the route is activated * This class represents a resolver that requests a specific process before the route is activated
*/ */
@@ -23,7 +27,7 @@ export class ProcessPageResolver implements Resolve<RemoteData<Process>> {
* or an error if something went wrong * or an error if something went wrong
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Process>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Process>> {
return this.processService.findById(route.params.id, false, true, followLink('script')).pipe( return this.processService.findById(route.params.id, false, true, ...PROCESS_PAGE_FOLLOW_LINKS).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
); );
} }

View File

@@ -13,6 +13,8 @@ import { RemoteData } from '../../core/data/remote-data';
import { SCRIPT } from '../scripts/script.resource-type'; import { SCRIPT } from '../scripts/script.resource-type';
import { Script } from '../scripts/script.model'; import { Script } from '../scripts/script.model';
import { CacheableObject } from '../../core/cache/cacheable-object.model'; import { CacheableObject } from '../../core/cache/cacheable-object.model';
import { BITSTREAM } from '../../core/shared/bitstream.resource-type';
import { PaginatedList } from '../../core/data/paginated-list.model';
/** /**
* Object representing a process * Object representing a process
@@ -94,4 +96,11 @@ export class Process implements CacheableObject {
*/ */
@link(PROCESS_OUTPUT_TYPE) @link(PROCESS_OUTPUT_TYPE)
output?: Observable<RemoteData<Bitstream>>; output?: Observable<RemoteData<Bitstream>>;
/**
* The files created by this Process
* Will be undefined unless the output {@link HALLink} has been resolved.
*/
@link(BITSTREAM, true)
files?: Observable<RemoteData<PaginatedList<Bitstream>>>;
} }