diff --git a/src/app/core/data/processes/process-data.service.ts b/src/app/core/data/processes/process-data.service.ts index e17b0b1f19..4cd18caad9 100644 --- a/src/app/core/data/processes/process-data.service.ts +++ b/src/app/core/data/processes/process-data.service.ts @@ -12,7 +12,7 @@ import { Bitstream } from '../../shared/bitstream.model'; import { RemoteData } from '../remote-data'; import { BitstreamDataService } from '../bitstream-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 { FindListOptions } from '../find-list-options.model'; import { dataService } from '../base/data-service.decorator'; @@ -35,21 +35,6 @@ export const TIMER_FACTORY = new InjectionToken<(callback: (...args: any[]) => v @Injectable() @dataService(PROCESS) export class ProcessDataService extends IdentifiableDataService implements FindAllData, DeleteData { - /** - * 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; private deleteData: DeleteData; @@ -71,6 +56,22 @@ export class ProcessDataService extends IdentifiableDataService impleme 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 * @param processId The ID of the process @@ -153,11 +154,14 @@ export class ProcessDataService extends IdentifiableDataService impleme * 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 * 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> { - const process$ = this.findById(processId, true, true, followLink('script')) + public autoRefreshUntilCompletion(processId: string, pollingIntervalInMs = 5000, ...linksToFollow: FollowLinkConfig[]): Observable> { + const process$: Observable> = this.findById(processId, true, true, ...linksToFollow) .pipe( getAllCompletedRemoteData(), ); diff --git a/src/app/process-page/detail/process-detail.component.spec.ts b/src/app/process-page/detail/process-detail.component.spec.ts index 241af9fd10..9ba5d6e94d 100644 --- a/src/app/process-page/detail/process-detail.component.spec.ts +++ b/src/app/process-page/detail/process-detail.component.spec.ts @@ -27,7 +27,6 @@ import { ProcessDataService } from '../../core/data/processes/process-data.servi import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { createFailedRemoteDataObject$, - createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; 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 { NotificationsService } from '../../shared/notifications/notifications.service'; 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', () => { let component: ProcessDetailComponent; @@ -44,44 +47,18 @@ describe('ProcessDetailComponent', () => { let nameService: DSONameService; let bitstreamDataService: BitstreamDataService; let httpClient: HttpClient; - let route: ActivatedRoute; + let route: ActivatedRouteStub; + let router: RouterStub; + let modalService; + let notificationsService: NotificationsServiceStub; let process: Process; let fileName: string; let files: Bitstream[]; - let processOutput; - - let modalService; - let notificationsService; - - let router; + let processOutput: string; 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'; files = [ 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(), { id: 'output.log', _links: { @@ -127,33 +131,22 @@ describe('ProcessDetailComponent', () => { notificationsService = new NotificationsServiceStub(); - router = jasmine.createSpyObj('router', { - navigateByUrl:{} - }); + router = new RouterStub(); - route = jasmine.createSpyObj('route', { - data: observableOf({ process: createSuccessfulRemoteDataObject$(process) }), - snapshot: { - params: { id: process.processId } - } + route = new ActivatedRouteStub({ + id: process.processId, + }, { + process: createSuccessfulRemoteDataObject$(process), }); } beforeEach(waitForAsync(() => { init(); - TestBed.configureTestingModule({ + void TestBed.configureTestingModule({ declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe], - imports: [TranslateModule.forRoot()], + imports: [TranslateModule.forRoot(), RouterTestingModule], providers: [ - { - provide: ActivatedRoute, - useValue: { - data: observableOf({ process: createSuccessfulRemoteDataObject(process) }), - snapshot: { - params: { id: process.processId } - } - } - }, + { provide: ActivatedRoute, useValue: route }, { provide: ProcessDataService, useValue: processService }, { provide: BitstreamDataService, useValue: bitstreamDataService }, { provide: DSONameService, useValue: nameService }, @@ -258,6 +251,8 @@ describe('ProcessDetailComponent', () => { describe('deleteProcess', () => { it('should delete the process and navigate back to the overview page on success', () => { spyOn(component, 'closeModal'); + spyOn(router, 'navigateByUrl').and.callThrough(); + component.deleteProcess(process); expect(processService.delete).toHaveBeenCalledWith(process.processId); @@ -268,6 +263,7 @@ describe('ProcessDetailComponent', () => { it('should delete the process and not navigate on error', () => { (processService.delete as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$()); spyOn(component, 'closeModal'); + spyOn(router, 'navigateByUrl').and.callThrough(); component.deleteProcess(process); diff --git a/src/app/process-page/detail/process-detail.component.ts b/src/app/process-page/detail/process-detail.component.ts index c8e4507fd2..e86797d66e 100644 --- a/src/app/process-page/detail/process-detail.component.ts +++ b/src/app/process-page/detail/process-detail.component.ts @@ -1,7 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Component, Inject, NgZone, OnInit, PLATFORM_ID } from '@angular/core'; 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 { AuthService } from '../../core/auth/auth.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 { TranslateService } from '@ngx-translate/core'; import { isPlatformBrowser } from '@angular/common'; +import { PROCESS_PAGE_FOLLOW_LINKS } from '../process-page.resolver'; @Component({ selector: 'ds-process-detail', @@ -84,8 +85,6 @@ export class ProcessDetailComponent implements OnInit { */ protected modalRef: NgbModalRef; - private refreshTimerSub?: Subscription; - constructor( @Inject(PLATFORM_ID) protected platformId: object, protected route: ActivatedRoute, @@ -109,7 +108,7 @@ export class ProcessDetailComponent implements OnInit { this.processRD$ = this.route.data.pipe( switchMap((data) => { 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 { return [data.process as RemoteData]; } @@ -125,7 +124,7 @@ export class ProcessDetailComponent implements OnInit { this.filesRD$ = this.processRD$.pipe( getAllSucceededRemoteDataPayload(), - switchMap((process: Process) => this.processService.getFiles(process.processId)) + switchMap((process: Process) => process.files), ); } diff --git a/src/app/process-page/process-page.resolver.ts b/src/app/process-page/process-page.resolver.ts index ba872302b3..2e4843646b 100644 --- a/src/app/process-page/process-page.resolver.ts +++ b/src/app/process-page/process-page.resolver.ts @@ -7,6 +7,10 @@ import { followLink } from '../shared/utils/follow-link-config.model'; import { ProcessDataService } from '../core/data/processes/process-data.service'; 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 */ @@ -23,7 +27,7 @@ export class ProcessPageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - 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(), ); } diff --git a/src/app/process-page/processes/process.model.ts b/src/app/process-page/processes/process.model.ts index d5f6e77d32..da396759e9 100644 --- a/src/app/process-page/processes/process.model.ts +++ b/src/app/process-page/processes/process.model.ts @@ -13,6 +13,8 @@ import { RemoteData } from '../../core/data/remote-data'; import { SCRIPT } from '../scripts/script.resource-type'; import { Script } from '../scripts/script.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 @@ -94,4 +96,11 @@ export class Process implements CacheableObject { */ @link(PROCESS_OUTPUT_TYPE) output?: Observable>; + + /** + * The files created by this Process + * Will be undefined unless the output {@link HALLink} has been resolved. + */ + @link(BITSTREAM, true) + files?: Observable>>; }