mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge pull request #2228 from enea4science/feature/DURACOM-131
[DURACOM-131] Show output files in separate lines and a refresh spinner if status is running
This commit is contained in:
@@ -1,10 +1,15 @@
|
|||||||
<div class="container" *ngVar="(processRD$ | async)?.payload as process">
|
<div class="container" *ngIf="(processRD$ | async)?.payload as process">
|
||||||
<div class="d-flex">
|
<div class="row">
|
||||||
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{
|
<div class="col-10">
|
||||||
id: process?.processId,
|
<h2 class="flex-grow-1">
|
||||||
name: process?.scriptName
|
{{ 'process.detail.title' | translate:{ id: process?.processId, name: process?.scriptName } }}
|
||||||
} }}</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<div *ngIf="refreshCounter$ | async as seconds" class="col-2 refresh-counter">
|
||||||
|
Refreshing in {{ seconds }}s <i class="fas fa-sync-alt fa-spin"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
|
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
|
||||||
<div>{{ process?.scriptName }}</div>
|
<div>{{ process?.scriptName }}</div>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
@@ -17,10 +22,12 @@
|
|||||||
<div *ngVar="(filesRD$ | async)?.payload?.page as files">
|
<div *ngVar="(filesRD$ | async)?.payload?.page as files">
|
||||||
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files"
|
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files"
|
||||||
[title]="'process.detail.output-files'">
|
[title]="'process.detail.output-files'">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
<ds-themed-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
|
<ds-themed-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
|
||||||
<span>{{getFileName(file)}}</span>
|
<span>{{getFileName(file)}}</span>
|
||||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||||
</ds-themed-file-download-link>
|
</ds-themed-file-download-link>
|
||||||
|
</div>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,7 +77,7 @@
|
|||||||
|
|
||||||
<ng-template #deleteModal >
|
<ng-template #deleteModal >
|
||||||
|
|
||||||
<div *ngVar="(processRD$ | async)?.payload as process">
|
<div *ngIf="(processRD$ | async)?.payload as process">
|
||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div>
|
<div>
|
||||||
|
@@ -35,6 +35,7 @@ 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 {ProcessStatus} from '../processes/process-status.model';
|
||||||
|
|
||||||
describe('ProcessDetailComponent', () => {
|
describe('ProcessDetailComponent', () => {
|
||||||
let component: ProcessDetailComponent;
|
let component: ProcessDetailComponent;
|
||||||
@@ -44,6 +45,7 @@ describe('ProcessDetailComponent', () => {
|
|||||||
let nameService: DSONameService;
|
let nameService: DSONameService;
|
||||||
let bitstreamDataService: BitstreamDataService;
|
let bitstreamDataService: BitstreamDataService;
|
||||||
let httpClient: HttpClient;
|
let httpClient: HttpClient;
|
||||||
|
let route: ActivatedRoute;
|
||||||
|
|
||||||
let process: Process;
|
let process: Process;
|
||||||
let fileName: string;
|
let fileName: string;
|
||||||
@@ -106,7 +108,8 @@ describe('ProcessDetailComponent', () => {
|
|||||||
});
|
});
|
||||||
processService = jasmine.createSpyObj('processService', {
|
processService = jasmine.createSpyObj('processService', {
|
||||||
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)),
|
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)),
|
||||||
delete: createSuccessfulRemoteDataObject$(null)
|
delete: createSuccessfulRemoteDataObject$(null),
|
||||||
|
findById: createSuccessfulRemoteDataObject$(process),
|
||||||
});
|
});
|
||||||
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||||
findByHref: createSuccessfulRemoteDataObject$(logBitstream)
|
findByHref: createSuccessfulRemoteDataObject$(logBitstream)
|
||||||
@@ -127,6 +130,13 @@ describe('ProcessDetailComponent', () => {
|
|||||||
router = jasmine.createSpyObj('router', {
|
router = jasmine.createSpyObj('router', {
|
||||||
navigateByUrl:{}
|
navigateByUrl:{}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
route = jasmine.createSpyObj('route', {
|
||||||
|
data: observableOf({ process: createSuccessfulRemoteDataObject(process) }),
|
||||||
|
snapshot: {
|
||||||
|
params: { id: process.processId }
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -263,4 +273,92 @@ describe('ProcessDetailComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('refresh counter', () => {
|
||||||
|
const queryRefreshCounter = () => fixture.debugElement.query(By.css('.refresh-counter'));
|
||||||
|
|
||||||
|
describe('if process is completed', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.processStatus = ProcessStatus.COMPLETED;
|
||||||
|
route.data = observableOf({process: createSuccessfulRemoteDataObject(process)});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show', () => {
|
||||||
|
spyOn(component, 'startRefreshTimer');
|
||||||
|
|
||||||
|
const refreshCounter = queryRefreshCounter();
|
||||||
|
expect(refreshCounter).toBeNull();
|
||||||
|
|
||||||
|
expect(component.startRefreshTimer).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if process is not finished', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.processStatus = ProcessStatus.RUNNING;
|
||||||
|
route.data = observableOf({process: createSuccessfulRemoteDataObject(process)});
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.stopRefreshTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call startRefreshTimer', () => {
|
||||||
|
spyOn(component, 'startRefreshTimer');
|
||||||
|
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges(); // subscribe to process observable with async pipe
|
||||||
|
|
||||||
|
expect(component.startRefreshTimer).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call refresh method every 5 seconds, until process is completed', fakeAsync(() => {
|
||||||
|
spyOn(component, 'refresh');
|
||||||
|
spyOn(component, 'stopRefreshTimer');
|
||||||
|
|
||||||
|
process.processStatus = ProcessStatus.COMPLETED;
|
||||||
|
// set findbyId to return a completed process
|
||||||
|
(processService.findById as jasmine.Spy).and.returnValue(observableOf(createSuccessfulRemoteDataObject(process)));
|
||||||
|
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges(); // subscribe to process observable with async pipe
|
||||||
|
|
||||||
|
expect(component.refresh).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(component.refreshCounter$.value).toBe(0);
|
||||||
|
|
||||||
|
tick(1001); // 1 second + 1 ms by the setTimeout
|
||||||
|
expect(component.refreshCounter$.value).toBe(5); // 5 - 0
|
||||||
|
|
||||||
|
tick(2001); // 2 seconds + 1 ms by the setTimeout
|
||||||
|
expect(component.refreshCounter$.value).toBe(3); // 5 - 2
|
||||||
|
|
||||||
|
tick(2001); // 2 seconds + 1 ms by the setTimeout
|
||||||
|
expect(component.refreshCounter$.value).toBe(1); // 3 - 2
|
||||||
|
|
||||||
|
tick(1001); // 1 second + 1 ms by the setTimeout
|
||||||
|
expect(component.refreshCounter$.value).toBe(0); // 1 - 1
|
||||||
|
|
||||||
|
tick(1000); // 1 second
|
||||||
|
|
||||||
|
expect(component.refresh).toHaveBeenCalledTimes(1);
|
||||||
|
expect(component.stopRefreshTimer).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(component.refreshCounter$.value).toBe(0);
|
||||||
|
|
||||||
|
tick(1001); // 1 second + 1 ms by the setTimeout
|
||||||
|
// startRefreshTimer not called again
|
||||||
|
expect(component.refreshCounter$.value).toBe(0);
|
||||||
|
|
||||||
|
discardPeriodicTasks(); // discard any periodic tasks that have not yet executed
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should show if refreshCounter is different from 0', () => {
|
||||||
|
component.refreshCounter$.next(1);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const refreshCounter = queryRefreshCounter();
|
||||||
|
expect(refreshCounter).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Component, NgZone, OnInit } from '@angular/core';
|
import { Component, Inject, NgZone, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, interval, Observable, shareReplay, Subscription } from 'rxjs';
|
||||||
import { finalize, map, switchMap, take, tap } from 'rxjs/operators';
|
import { finalize, map, switchMap, take, tap } 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';
|
||||||
@@ -26,6 +26,8 @@ import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
import { getProcessListRoute } from '../process-page-routing.paths';
|
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 { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-process-detail',
|
selector: 'ds-process-detail',
|
||||||
@@ -34,7 +36,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
/**
|
/**
|
||||||
* A component displaying detailed information about a DSpace Process
|
* A component displaying detailed information about a DSpace Process
|
||||||
*/
|
*/
|
||||||
export class ProcessDetailComponent implements OnInit {
|
export class ProcessDetailComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The AlertType enumeration
|
* The AlertType enumeration
|
||||||
@@ -65,23 +67,29 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Boolean on whether or not to show the output logs
|
* Boolean on whether or not to show the output logs
|
||||||
*/
|
*/
|
||||||
showOutputLogs;
|
showOutputLogs = false;
|
||||||
/**
|
/**
|
||||||
* When it's retrieving the output logs from backend, to show loading component
|
* When it's retrieving the output logs from backend, to show loading component
|
||||||
*/
|
*/
|
||||||
retrievingOutputLogs$: BehaviorSubject<boolean>;
|
retrievingOutputLogs$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Date format to use for start and end time of processes
|
* Date format to use for start and end time of processes
|
||||||
*/
|
*/
|
||||||
dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ';
|
dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ';
|
||||||
|
|
||||||
|
refreshCounter$ = new BehaviorSubject(0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to NgbModal
|
* Reference to NgbModal
|
||||||
*/
|
*/
|
||||||
protected modalRef: NgbModalRef;
|
protected modalRef: NgbModalRef;
|
||||||
|
|
||||||
constructor(protected route: ActivatedRoute,
|
private refreshTimerSub?: Subscription;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(PLATFORM_ID) protected platformId: object,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected processService: ProcessDataService,
|
protected processService: ProcessDataService,
|
||||||
protected bitstreamDataService: BitstreamDataService,
|
protected bitstreamDataService: BitstreamDataService,
|
||||||
@@ -92,21 +100,25 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
protected modalService: NgbModal,
|
protected modalService: NgbModal,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected translateService: TranslateService
|
protected translateService: TranslateService
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize component properties
|
* Initialize component properties
|
||||||
* Display a 404 if the process doesn't exist
|
* Display a 404 if the process doesn't exist
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.showOutputLogs = false;
|
|
||||||
this.retrievingOutputLogs$ = new BehaviorSubject<boolean>(false);
|
|
||||||
this.processRD$ = this.route.data.pipe(
|
this.processRD$ = this.route.data.pipe(
|
||||||
map((data) => {
|
map((data) => {
|
||||||
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
|
if (!this.isProcessFinished(data.process.payload)) {
|
||||||
|
this.startRefreshTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return data.process as RemoteData<Process>;
|
return data.process as RemoteData<Process>;
|
||||||
}),
|
}),
|
||||||
redirectOn4xx(this.router, this.authService)
|
redirectOn4xx(this.router, this.authService),
|
||||||
|
shareReplay(1)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.filesRD$ = this.processRD$.pipe(
|
this.filesRD$ = this.processRD$.pipe(
|
||||||
@@ -115,6 +127,53 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.processRD$ = this.processService.findById(
|
||||||
|
this.route.snapshot.params.id,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
followLink('script')
|
||||||
|
).pipe(
|
||||||
|
getFirstSucceededRemoteData(),
|
||||||
|
redirectOn4xx(this.router, this.authService),
|
||||||
|
tap((processRemoteData: RemoteData<Process>) => {
|
||||||
|
if (!this.isProcessFinished(processRemoteData.payload)) {
|
||||||
|
this.startRefreshTimer();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.filesRD$ = this.processRD$.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
switchMap((process: Process) => this.processService.getFiles(process.processId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
startRefreshTimer() {
|
||||||
|
this.refreshCounter$.next(0);
|
||||||
|
|
||||||
|
this.refreshTimerSub = interval(1000).subscribe(
|
||||||
|
value => {
|
||||||
|
if (value > 5) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.refresh();
|
||||||
|
this.stopRefreshTimer();
|
||||||
|
this.refreshCounter$.next(0);
|
||||||
|
}, 1);
|
||||||
|
} else {
|
||||||
|
this.refreshCounter$.next(5 - value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stopRefreshTimer() {
|
||||||
|
if (hasValue(this.refreshTimerSub)) {
|
||||||
|
this.refreshTimerSub.unsubscribe();
|
||||||
|
this.refreshTimerSub = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the name of a bitstream
|
* Get the name of a bitstream
|
||||||
* @param bitstream
|
* @param bitstream
|
||||||
@@ -210,6 +269,7 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
openDeleteModal(content) {
|
openDeleteModal(content) {
|
||||||
this.modalRef = this.modalService.open(content);
|
this.modalRef = this.modalService.open(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the modal.
|
* Close the modal.
|
||||||
*/
|
*/
|
||||||
@@ -217,4 +277,7 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
this.modalRef.close();
|
this.modalRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.stopRefreshTimer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user