Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for process output on detail page #827

Merged
merged 7 commits into from
Nov 20, 2020
4 changes: 4 additions & 0 deletions src/app/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -281,6 +283,7 @@ const PROVIDERS = [
ItemTypeDataService,
WorkflowActionDataService,
ProcessDataService,
ProcessOutputDataService,
ScriptDataService,
ProcessFilesResponseParsingService,
FeatureDataService,
Expand Down Expand Up @@ -347,6 +350,7 @@ export const models =
ExternalSourceEntry,
Script,
Process,
ProcessOutput,
Version,
VersionHistory,
WorkflowAction,
Expand Down
72 changes: 72 additions & 0 deletions src/app/core/data/process-output-data.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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<ProcessOutput> {
protected linkPath = 'processes';

constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<ProcessOutput>) {
super();
}
}

/**
* 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<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<ProcessOutput>) {
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<FollowLinkConfig<ProcessOutput>>): Observable<RemoteData<ProcessOutput>> {
return this.dataService.findByHref(href, ...linksToFollow);
}
}
/* tslint:enable:max-classes-per-file */
9 changes: 9 additions & 0 deletions src/app/core/shared/process-output.resource-type.ts
Original file line number Diff line number Diff line change
@@ -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');
18 changes: 14 additions & 4 deletions src/app/process-page/detail/process-detail.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,19 @@ <h2 class="flex-grow-1">{{'process.detail.title' | translate:{id: process?.proce
<div>{{ process.processStatus }}</div>
</ds-process-detail-field>

<!--<ds-process-detail-field id="process-output" [title]="'process.detail.output'">-->
<!--<pre class="font-weight-bold text-secondary bg-light p-3">{{'process.detail.output.alert' | translate}}</pre>-->
<!--</ds-process-detail-field>-->
<ds-process-detail-field id="process-output" [title]="'process.detail.output'">
<button *ngIf="!showOutputLogs" id="showOutputButton" class="btn btn-light" (click)="showProcessOutputLogs()">
{{ 'process.detail.logs.button' | translate }}
</button>
<ds-loading *ngIf="retrievingOutputLogs$ | async" class="ds-loading" message="{{ 'process.detail.logs.loading' | translate }}"></ds-loading>
<pre class="font-weight-bold text-secondary bg-light p-3"
*ngIf="showOutputLogs && (outputLogs$ | async)?.length > 0">{{ (outputLogs$ | async)?.join('\n') }}</pre>
<p id="no-output-logs-message" *ngIf="showOutputLogs && !(outputLogs$ | async) || (outputLogs$ | async)?.length == 0">
{{ 'process.detail.logs.none' | translate }}
</p>
</ds-process-detail-field>

<a class="btn btn-light mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
<div>
<a class="btn btn-light mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
</div>
</div>
85 changes: 81 additions & 4 deletions src/app/process-page/detail/process-detail.component.spec.ts
Original file line number Diff line number Diff line change
@@ -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, tick } from '@angular/core/testing';
import { VarDirective } from '../../shared/utils/var.directive';
import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing';
Expand All @@ -21,13 +23,20 @@ describe('ProcessDetailComponent', () => {
let fixture: ComponentFixture<ProcessDetailComponent>;

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',
Expand All @@ -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 = [
Expand All @@ -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
});
Expand All @@ -73,8 +93,12 @@ 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 }
],
schemas: [NO_ERRORS_SCHEMA]
Expand All @@ -84,24 +108,77 @@ 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}`)
});
});

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);
});

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 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();
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();
});
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();
});
});

});
51 changes: 47 additions & 4 deletions src/app/process-page/detail/process-detail.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +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';
Expand Down Expand Up @@ -36,10 +40,26 @@ export class ProcessDetailComponent implements OnInit {
*/
filesRD$: Observable<RemoteData<PaginatedList<Bitstream>>>;

/**
* The Process's Output logs
*/
outputLogs$: Observable<string[]>;

/**
* 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<boolean>(false);

constructor(protected route: ActivatedRoute,
protected router: Router,
protected processService: ProcessDataService,
protected nameService: DSONameService) {
protected processOutputService: ProcessOutputDataService,
protected nameService: DSONameService,
private zone: NgZone) {
}

/**
Expand All @@ -63,7 +83,30 @@ export class ProcessDetailComponent implements OnInit {
* @param bitstream
*/
getFileName(bitstream: Bitstream) {
return this.nameService.getName(bitstream);
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<RemoteData<ProcessOutput>> = 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();
}

}