Skip to content

Commit

Permalink
Merge f82e6fa into 16a68da
Browse files Browse the repository at this point in the history
  • Loading branch information
MarieVerdonck committed Sep 2, 2020
2 parents 16a68da + f82e6fa commit 3c0d899
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 13 deletions.
4 changes: 4 additions & 0 deletions src/app/core/core.module.ts
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
@@ -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
@@ -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
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
@@ -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
@@ -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();
}

}

0 comments on commit 3c0d899

Please sign in to comment.