From 8325a34910aea6be0de0e4b61ba8a6df3a5fe17c Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Tue, 4 Aug 2020 14:15:25 +0200 Subject: [PATCH 1/4] 72403: Support for process output on detail page --- src/app/core/core.module.ts | 4 + .../core/data/process-output-data.service.ts | 73 +++++++++++++++++++ .../shared/process-output.resource-type.ts | 9 +++ .../detail/process-detail.component.html | 13 +++- .../detail/process-detail.component.spec.ts | 53 +++++++++++++- .../detail/process-detail.component.ts | 24 +++++- src/app/process-page/process-page.resolver.ts | 2 +- .../processes/process-output.model.ts | 36 +++++++++ .../process-page/processes/process.model.ts | 9 +++ src/assets/i18n/en.json5 | 2 +- 10 files changed, 216 insertions(+), 9 deletions(-) create mode 100644 src/app/core/data/process-output-data.service.ts create mode 100644 src/app/core/shared/process-output.resource-type.ts create mode 100644 src/app/process-page/processes/process-output.model.ts diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 5aa462d5e0e..d262bfd0d67 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -6,6 +6,7 @@ import { EffectsModule } from '@ngrx/effects'; import { Action, StoreConfig, StoreModule } from '@ngrx/store'; import { MyDSpaceGuard } from '../+my-dspace-page/my-dspace.guard'; +import { ProcessOutput } from '../process-page/processes/process-output.model'; import { isNotEmpty } from '../shared/empty.util'; import { FormBuilderService } from '../shared/form/builder/form-builder.service'; @@ -70,6 +71,7 @@ import { LookupRelationService } from './data/lookup-relation.service'; import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service'; import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; +import { ProcessOutputDataService } from './data/process-output-data.service'; import { RelationshipTypeService } from './data/relationship-type.service'; import { RelationshipService } from './data/relationship.service'; import { ResourcePolicyService } from './resource-policy/resource-policy.service'; @@ -281,6 +283,7 @@ const PROVIDERS = [ ItemTypeDataService, WorkflowActionDataService, ProcessDataService, + ProcessOutputDataService, ScriptDataService, ProcessFilesResponseParsingService, FeatureDataService, @@ -347,6 +350,7 @@ export const models = ExternalSourceEntry, Script, Process, + ProcessOutput, Version, VersionHistory, WorkflowAction, diff --git a/src/app/core/data/process-output-data.service.ts b/src/app/core/data/process-output-data.service.ts new file mode 100644 index 00000000000..28adbcd6cab --- /dev/null +++ b/src/app/core/data/process-output-data.service.ts @@ -0,0 +1,73 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { ProcessOutput } from '../../process-page/processes/process-output.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { PROCESS_OUTPUT_TYPE } from '../shared/process-output.resource-type'; +import { DataService } from './data.service'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; + +/* tslint:disable:max-classes-per-file */ +/** + * A private DataService implementation to delegate specific methods to. + */ +class DataServiceImpl extends DataService { + protected linkPath = 'processes'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } +} + +// @ts-ignore +/** + * A service to retrieve output from processes from the REST API. + */ +@Injectable() +@dataService(PROCESS_OUTPUT_TYPE) +export class ProcessOutputDataService { + /** + * A private DataService instance to delegate specific methods to. + */ + private dataService: DataServiceImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); + } + + /** + * Returns an observable of {@link RemoteData} of a {@link ProcessOutput}, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the {@link ProcessOutput} + * @param href The url of {@link ProcessOutput} we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findByHref(href: string, ...linksToFollow: Array>): Observable> { + return this.dataService.findByHref(href, ...linksToFollow); + } +} +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/shared/process-output.resource-type.ts b/src/app/core/shared/process-output.resource-type.ts new file mode 100644 index 00000000000..2e707d0bdad --- /dev/null +++ b/src/app/core/shared/process-output.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for ProcessOutput + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const PROCESS_OUTPUT_TYPE = new ResourceType('processOutput'); diff --git a/src/app/process-page/detail/process-detail.component.html b/src/app/process-page/detail/process-detail.component.html index 9cb1f1e6afa..078c79a0d5b 100644 --- a/src/app/process-page/detail/process-detail.component.html +++ b/src/app/process-page/detail/process-detail.component.html @@ -34,9 +34,16 @@

{{'process.detail.title' | translate:{id: process?.proce
{{ process.processStatus }}
- - - + +
+      
+        {{ (outputLogs$ | async)?.join('\n\t') }}
+      
+    
+

+ {{ 'process.detail.logs.none' | translate }} +

+
{{'process.detail.back' | translate}} 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 dff481fdc61..c1efd233e83 100644 --- a/src/app/process-page/detail/process-detail.component.spec.ts +++ b/src/app/process-page/detail/process-detail.component.spec.ts @@ -1,5 +1,7 @@ +import { ProcessOutputDataService } from '../../core/data/process-output-data.service'; +import { ProcessOutput } from '../processes/process-output.model'; import { ProcessDetailComponent } from './process-detail.component'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; import { VarDirective } from '../../shared/utils/var.directive'; import { TranslateModule } from '@ngx-translate/core'; import { RouterTestingModule } from '@angular/router/testing'; @@ -21,13 +23,20 @@ describe('ProcessDetailComponent', () => { let fixture: ComponentFixture; let processService: ProcessDataService; + let processOutputService: ProcessOutputDataService; let nameService: DSONameService; let process: Process; let fileName: string; let files: Bitstream[]; + let processOutput; + function init() { + processOutput = Object.assign(new ProcessOutput(), { + logs: ['Process started', 'Process completed'] + } + ); process = Object.assign(new Process(), { processId: 1, scriptName: 'script-name', @@ -40,7 +49,15 @@ describe('ProcessDetailComponent', () => { 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 = [ @@ -62,6 +79,9 @@ describe('ProcessDetailComponent', () => { processService = jasmine.createSpyObj('processService', { getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)) }); + processOutputService = jasmine.createSpyObj('processOutputService', { + findByHref: createSuccessfulRemoteDataObject$(processOutput) + }); nameService = jasmine.createSpyObj('nameService', { getName: fileName }); @@ -75,6 +95,7 @@ describe('ProcessDetailComponent', () => { providers: [ { provide: ActivatedRoute, useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) } }, { provide: ProcessDataService, useValue: processService }, + { provide: ProcessOutputDataService, useValue: processOutputService }, { provide: DSONameService, useValue: nameService } ], schemas: [NO_ERRORS_SCHEMA] @@ -104,4 +125,32 @@ describe('ProcessDetailComponent', () => { expect(processFiles.textContent).toContain(fileName); }); + it('should display the process\'s output logs', () => { + const outputProcess = fixture.debugElement.query(By.css('#process-output pre')).nativeElement; + expect(outputProcess.textContent).toContain('Process started'); + }); + + describe('if process has no output logs (yet)', () => { + beforeEach(fakeAsync(() => { + jasmine.getEnv().allowRespy(true); + const emptyProcessOutput = Object.assign(new ProcessOutput(), { + logs: [] + }); + spyOn(processOutputService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(emptyProcessOutput)); + fixture = TestBed.createComponent(ProcessDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }) + ); + it('should not display the process\'s output logs', () => { + const outputProcess = fixture.debugElement.query(By.css('#process-output pre')); + expect(outputProcess).toBeNull(); + }); + it('should display message saying there are no output logs', () => { + const noOutputProcess = fixture.debugElement.query(By.css('#no-output-logs-message')).nativeElement; + expect(noOutputProcess).toBeDefined(); + }); + } + ) + }); diff --git a/src/app/process-page/detail/process-detail.component.ts b/src/app/process-page/detail/process-detail.component.ts index b0e2c7e378c..74027a4e72a 100644 --- a/src/app/process-page/detail/process-detail.component.ts +++ b/src/app/process-page/detail/process-detail.component.ts @@ -1,9 +1,12 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs/internal/Observable'; +import { ProcessOutputDataService } from '../../core/data/process-output-data.service'; import { RemoteData } from '../../core/data/remote-data'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { ProcessOutput } from '../processes/process-output.model'; import { Process } from '../processes/process.model'; -import { map, switchMap } from 'rxjs/operators'; +import { map, switchMap, tap } from 'rxjs/operators'; import { getFirstSucceededRemoteDataPayload, redirectToPageNotFoundOn404 } from '../../core/shared/operators'; import { AlertType } from '../../shared/alert/aletr-type'; import { ProcessDataService } from '../../core/data/processes/process-data.service'; @@ -36,9 +39,15 @@ export class ProcessDetailComponent implements OnInit { */ filesRD$: Observable>>; + /** + * The Process's Output logs + */ + outputLogs$: Observable; + constructor(protected route: ActivatedRoute, protected router: Router, protected processService: ProcessDataService, + protected processOutputService: ProcessOutputDataService, protected nameService: DSONameService) { } @@ -56,6 +65,17 @@ export class ProcessDetailComponent implements OnInit { getFirstSucceededRemoteDataPayload(), switchMap((process: Process) => this.processService.getFiles(process.processId)) ); + + const processOutputRD$: Observable> = this.processRD$.pipe( + getFirstSucceededRemoteDataPayload(), + switchMap((process: Process) => this.processOutputService.findByHref(process._links.output.href)) + ); + this.outputLogs$ = processOutputRD$.pipe( + getFirstSucceededRemoteDataPayload(), + switchMap((processOutput: ProcessOutput) => { + return [processOutput.logs]; + }) + ) } /** @@ -63,7 +83,7 @@ export class ProcessDetailComponent implements OnInit { * @param bitstream */ getFileName(bitstream: Bitstream) { - return this.nameService.getName(bitstream); + return bitstream instanceof DSpaceObject ? this.nameService.getName(bitstream) : 'unknown'; } } diff --git a/src/app/process-page/process-page.resolver.ts b/src/app/process-page/process-page.resolver.ts index 84821a2574c..57c749e1cbb 100644 --- a/src/app/process-page/process-page.resolver.ts +++ b/src/app/process-page/process-page.resolver.ts @@ -24,7 +24,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, followLink('script')).pipe( + return this.processService.findById(route.params.id, followLink('script'), followLink('output') ).pipe( find((RD) => hasValue(RD.error) || RD.hasSucceeded), ); } diff --git a/src/app/process-page/processes/process-output.model.ts b/src/app/process-page/processes/process-output.model.ts new file mode 100644 index 00000000000..4ae1731d26a --- /dev/null +++ b/src/app/process-page/processes/process-output.model.ts @@ -0,0 +1,36 @@ +import { PROCESS_OUTPUT_TYPE } from '../../core/shared/process-output.resource-type'; +import { CacheableObject } from '../../core/cache/object-cache.reducer'; +import { HALLink } from '../../core/shared/hal-link.model'; +import { autoserialize, deserialize } from 'cerialize'; +import { excludeFromEquals } from '../../core/utilities/equals.decorators'; +import { ResourceType } from '../../core/shared/resource-type'; +import { typedObject } from '../../core/cache/builders/build-decorators'; + +/** + * Object representing a process output object + */ +@typedObject +export class ProcessOutput implements CacheableObject { + static type = PROCESS_OUTPUT_TYPE; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The output strings for this ProcessOutput + */ + @autoserialize + logs: string[]; + + /** + * The {@link HALLink}s for this ProcessOutput + */ + @deserialize + _links: { + self: HALLink, + }; +} diff --git a/src/app/process-page/processes/process.model.ts b/src/app/process-page/processes/process.model.ts index 85de5337e78..891acb626dd 100644 --- a/src/app/process-page/processes/process.model.ts +++ b/src/app/process-page/processes/process.model.ts @@ -1,3 +1,5 @@ +import { PROCESS_OUTPUT_TYPE } from '../../core/shared/process-output.resource-type'; +import { ProcessOutput } from './process-output.model'; import { ProcessStatus } from './process-status.model'; import { ProcessParameter } from './process-parameter.model'; import { CacheableObject } from '../../core/cache/object-cache.reducer'; @@ -85,4 +87,11 @@ export class Process implements CacheableObject { */ @link(SCRIPT) script?: Observable>; + + /** + * The output logs created by this Process + * Will be undefined unless the output {@link HALLink} has been resolved. + */ + @link(PROCESS_OUTPUT_TYPE) + output?: Observable>; } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index bfa1c81aa69..69e7df151ea 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2085,7 +2085,7 @@ "process.detail.output" : "Process Output", - "process.detail.output.alert" : "Work in progress - Process output is not available yet", + "process.detail.logs.none": "This process has no output logs (yet)", "process.detail.output-files" : "Output Files", From f2a381643028d4027b03df6f9123eda50aa2a321 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Tue, 4 Aug 2020 14:57:57 +0200 Subject: [PATCH 2/4] 72403: tab removed process output, message change & redundant switchmap removed --- src/app/process-page/detail/process-detail.component.html | 7 ++----- src/app/process-page/detail/process-detail.component.ts | 6 +++--- src/assets/i18n/en.json5 | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/app/process-page/detail/process-detail.component.html b/src/app/process-page/detail/process-detail.component.html index 078c79a0d5b..e76d24d17aa 100644 --- a/src/app/process-page/detail/process-detail.component.html +++ b/src/app/process-page/detail/process-detail.component.html @@ -35,11 +35,8 @@

{{'process.detail.title' | translate:{id: process?.proce -
-      
-        {{ (outputLogs$ | async)?.join('\n\t') }}
-      
-    
+
{{ (outputLogs$ | async)?.join('\n') }}

{{ 'process.detail.logs.none' | translate }}

diff --git a/src/app/process-page/detail/process-detail.component.ts b/src/app/process-page/detail/process-detail.component.ts index 74027a4e72a..af6e3238a62 100644 --- a/src/app/process-page/detail/process-detail.component.ts +++ b/src/app/process-page/detail/process-detail.component.ts @@ -6,7 +6,7 @@ import { RemoteData } from '../../core/data/remote-data'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { ProcessOutput } from '../processes/process-output.model'; import { Process } from '../processes/process.model'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { getFirstSucceededRemoteDataPayload, redirectToPageNotFoundOn404 } from '../../core/shared/operators'; import { AlertType } from '../../shared/alert/aletr-type'; import { ProcessDataService } from '../../core/data/processes/process-data.service'; @@ -72,8 +72,8 @@ export class ProcessDetailComponent implements OnInit { ); this.outputLogs$ = processOutputRD$.pipe( getFirstSucceededRemoteDataPayload(), - switchMap((processOutput: ProcessOutput) => { - return [processOutput.logs]; + map((processOutput: ProcessOutput) => { + return processOutput.logs; }) ) } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 69e7df151ea..75db3eceb18 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2085,7 +2085,7 @@ "process.detail.output" : "Process Output", - "process.detail.logs.none": "This process has no output logs (yet)", + "process.detail.logs.none": "This process has no output yet", "process.detail.output-files" : "Output Files", From 02c693363b6f5ed28202898a2cc9c5c21c269f51 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Thu, 13 Aug 2020 12:31:42 +0200 Subject: [PATCH 3/4] Unneeded ts-ignore removed --- src/app/core/data/process-output-data.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/core/data/process-output-data.service.ts b/src/app/core/data/process-output-data.service.ts index 28adbcd6cab..24f33a85b79 100644 --- a/src/app/core/data/process-output-data.service.ts +++ b/src/app/core/data/process-output-data.service.ts @@ -36,7 +36,6 @@ class DataServiceImpl extends DataService { } } -// @ts-ignore /** * A service to retrieve output from processes from the REST API. */ From f82e6fa48bad7214b53f424bbf8b313152ae5ee9 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Wed, 2 Sep 2020 19:35:35 +0200 Subject: [PATCH 4/4] 72403: Process output logs only retrieved at button press + tests --- .../detail/process-detail.component.html | 22 ++++--- .../detail/process-detail.component.spec.ts | 66 +++++++++++++------ .../detail/process-detail.component.ts | 51 ++++++++++---- src/app/process-page/process-page.resolver.ts | 2 +- src/assets/i18n/en.json5 | 4 ++ 5 files changed, 103 insertions(+), 42 deletions(-) diff --git a/src/app/process-page/detail/process-detail.component.html b/src/app/process-page/detail/process-detail.component.html index e76d24d17aa..e13770a0a38 100644 --- a/src/app/process-page/detail/process-detail.component.html +++ b/src/app/process-page/detail/process-detail.component.html @@ -34,13 +34,19 @@

{{'process.detail.title' | translate:{id: process?.proce
{{ process.processStatus }}
- -
{{ (outputLogs$ | async)?.join('\n') }}
-

- {{ 'process.detail.logs.none' | translate }} -

-
+ + + +
{{ (outputLogs$ | async)?.join('\n') }}
+

+ {{ 'process.detail.logs.none' | translate }} +

+
- {{'process.detail.back' | translate}} + 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 c1efd233e83..8d4a3f0e621 100644 --- a/src/app/process-page/detail/process-detail.component.spec.ts +++ b/src/app/process-page/detail/process-detail.component.spec.ts @@ -1,7 +1,7 @@ import { ProcessOutputDataService } from '../../core/data/process-output-data.service'; import { ProcessOutput } from '../processes/process-output.model'; import { ProcessDetailComponent } from './process-detail.component'; -import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { VarDirective } from '../../shared/utils/var.directive'; import { TranslateModule } from '@ngx-translate/core'; import { RouterTestingModule } from '@angular/router/testing'; @@ -93,7 +93,10 @@ describe('ProcessDetailComponent', () => { declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], providers: [ - { provide: ActivatedRoute, useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) } }, + { + provide: ActivatedRoute, + useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) } + }, { provide: ProcessDataService, useValue: processService }, { provide: ProcessOutputDataService, useValue: processOutputService }, { provide: DSONameService, useValue: nameService } @@ -105,15 +108,16 @@ describe('ProcessDetailComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(ProcessDetailComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should display the script\'s name', () => { + fixture.detectChanges(); const name = fixture.debugElement.query(By.css('#process-name')).nativeElement; expect(name.textContent).toContain(process.scriptName); }); it('should display the process\'s parameters', () => { + fixture.detectChanges(); const args = fixture.debugElement.query(By.css('#process-arguments')).nativeElement; process.parameters.forEach((param) => { expect(args.textContent).toContain(`${param.name} ${param.value}`) @@ -121,27 +125,52 @@ describe('ProcessDetailComponent', () => { }); it('should display the process\'s output files', () => { + fixture.detectChanges(); const processFiles = fixture.debugElement.query(By.css('#process-files')).nativeElement; expect(processFiles.textContent).toContain(fileName); }); - it('should display the process\'s output logs', () => { - const outputProcess = fixture.debugElement.query(By.css('#process-output pre')).nativeElement; - expect(outputProcess.textContent).toContain('Process started'); + describe('if press show output logs', () => { + beforeEach(fakeAsync(() => { + spyOn(component, 'showProcessOutputLogs').and.callThrough(); + fixture.detectChanges(); + const showOutputButton = fixture.debugElement.query(By.css('#showOutputButton')); + showOutputButton.triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + })); + it('should trigger showProcessOutputLogs', () => { + expect(component.showProcessOutputLogs).toHaveBeenCalled(); + }); + it('should display the process\'s output logs', () => { + fixture.detectChanges(); + const outputProcess = fixture.debugElement.query(By.css('#process-output pre')); + expect(outputProcess.nativeElement.textContent).toContain('Process started'); + }); }); - describe('if process has no output logs (yet)', () => { + describe('if press show output logs and process has no output logs (yet)', () => { beforeEach(fakeAsync(() => { - jasmine.getEnv().allowRespy(true); - const emptyProcessOutput = Object.assign(new ProcessOutput(), { - logs: [] - }); - spyOn(processOutputService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(emptyProcessOutput)); - fixture = TestBed.createComponent(ProcessDetailComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }) - ); + jasmine.getEnv().allowRespy(true); + const emptyProcessOutput = Object.assign(new ProcessOutput(), { + logs: [] + }); + spyOn(processOutputService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(emptyProcessOutput)); + fixture = TestBed.createComponent(ProcessDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + spyOn(component, 'showProcessOutputLogs').and.callThrough(); + fixture.detectChanges(); + const showOutputButton = fixture.debugElement.query(By.css('#showOutputButton')); + showOutputButton.triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + fixture.detectChanges(); + })); it('should not display the process\'s output logs', () => { const outputProcess = fixture.debugElement.query(By.css('#process-output pre')); expect(outputProcess).toBeNull(); @@ -150,7 +179,6 @@ describe('ProcessDetailComponent', () => { const noOutputProcess = fixture.debugElement.query(By.css('#no-output-logs-message')).nativeElement; expect(noOutputProcess).toBeDefined(); }); - } - ) + }); }); diff --git a/src/app/process-page/detail/process-detail.component.ts b/src/app/process-page/detail/process-detail.component.ts index af6e3238a62..f6b628f0f80 100644 --- a/src/app/process-page/detail/process-detail.component.ts +++ b/src/app/process-page/detail/process-detail.component.ts @@ -1,12 +1,13 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, NgZone, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { Observable } from 'rxjs/internal/Observable'; import { ProcessOutputDataService } from '../../core/data/process-output-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { ProcessOutput } from '../processes/process-output.model'; import { Process } from '../processes/process.model'; -import { map, switchMap } from 'rxjs/operators'; +import { finalize, map, switchMap, take } from 'rxjs/operators'; import { getFirstSucceededRemoteDataPayload, redirectToPageNotFoundOn404 } from '../../core/shared/operators'; import { AlertType } from '../../shared/alert/aletr-type'; import { ProcessDataService } from '../../core/data/processes/process-data.service'; @@ -44,11 +45,21 @@ export class ProcessDetailComponent implements OnInit { */ outputLogs$: Observable; + /** + * Boolean on whether or not to show the output logs + */ + showOutputLogs = false; + /** + * When it's retrieving the output logs from backend, to show loading component + */ + retrievingOutputLogs$ = new BehaviorSubject(false); + constructor(protected route: ActivatedRoute, protected router: Router, protected processService: ProcessDataService, protected processOutputService: ProcessOutputDataService, - protected nameService: DSONameService) { + protected nameService: DSONameService, + private zone: NgZone) { } /** @@ -65,17 +76,6 @@ export class ProcessDetailComponent implements OnInit { getFirstSucceededRemoteDataPayload(), switchMap((process: Process) => this.processService.getFiles(process.processId)) ); - - const processOutputRD$: Observable> = this.processRD$.pipe( - getFirstSucceededRemoteDataPayload(), - switchMap((process: Process) => this.processOutputService.findByHref(process._links.output.href)) - ); - this.outputLogs$ = processOutputRD$.pipe( - getFirstSucceededRemoteDataPayload(), - map((processOutput: ProcessOutput) => { - return processOutput.logs; - }) - ) } /** @@ -86,4 +86,27 @@ export class ProcessDetailComponent implements OnInit { return bitstream instanceof DSpaceObject ? this.nameService.getName(bitstream) : 'unknown'; } + /** + * Retrieves the process logs, while setting the loading subject to true. + * Sets the outputLogs when retrieved and sets the showOutputLogs boolean to show them and hide the button. + */ + showProcessOutputLogs() { + this.retrievingOutputLogs$.next(true); + this.zone.runOutsideAngular(() => { + const processOutputRD$: Observable> = this.processRD$.pipe( + getFirstSucceededRemoteDataPayload(), + switchMap((process: Process) => this.processOutputService.findByHref(process._links.output.href)) + ); + this.outputLogs$ = processOutputRD$.pipe( + getFirstSucceededRemoteDataPayload(), + map((processOutput: ProcessOutput) => { + this.showOutputLogs = true; + return processOutput.logs; + }), + finalize(() => this.zone.run(() => this.retrievingOutputLogs$.next(false))), + ) + }); + this.outputLogs$.pipe(take(1)).subscribe(); + } + } diff --git a/src/app/process-page/process-page.resolver.ts b/src/app/process-page/process-page.resolver.ts index 57c749e1cbb..84821a2574c 100644 --- a/src/app/process-page/process-page.resolver.ts +++ b/src/app/process-page/process-page.resolver.ts @@ -24,7 +24,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, followLink('script'), followLink('output') ).pipe( + return this.processService.findById(route.params.id, followLink('script')).pipe( find((RD) => hasValue(RD.error) || RD.hasSucceeded), ); } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 75db3eceb18..036fd87ec8d 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2085,6 +2085,10 @@ "process.detail.output" : "Process Output", + "process.detail.logs.button": "Retrieve process output", + + "process.detail.logs.loading": "Retrieving", + "process.detail.logs.none": "This process has no output yet", "process.detail.output-files" : "Output Files",