Skip to content

Commit e2b3f52

Browse files
authored
Merge pull request DSpace#2228 from enea4science/feature/DURACOM-131
[DURACOM-131] Show output files in separate lines and a refresh spinner if status is running
2 parents 5af9793 + 3fdef20 commit e2b3f52

3 files changed

Lines changed: 199 additions & 31 deletions

File tree

src/app/process-page/detail/process-detail.component.html

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
<div class="container" *ngVar="(processRD$ | async)?.payload as process">
2-
<div class="d-flex">
3-
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{
4-
id: process?.processId,
5-
name: process?.scriptName
6-
} }}</h2>
1+
<div class="container" *ngIf="(processRD$ | async)?.payload as process">
2+
<div class="row">
3+
<div class="col-10">
4+
<h2 class="flex-grow-1">
5+
{{ 'process.detail.title' | translate:{ id: process?.processId, name: process?.scriptName } }}
6+
</h2>
7+
</div>
8+
<div *ngIf="refreshCounter$ | async as seconds" class="col-2 refresh-counter">
9+
Refreshing in {{ seconds }}s <i class="fas fa-sync-alt fa-spin"></i>
10+
</div>
711
</div>
12+
813
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
914
<div>{{ process?.scriptName }}</div>
1015
</ds-process-detail-field>
@@ -17,10 +22,12 @@ <h2 class="flex-grow-1">{{'process.detail.title' | translate:{
1722
<div *ngVar="(filesRD$ | async)?.payload?.page as files">
1823
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files"
1924
[title]="'process.detail.output-files'">
25+
<div class="d-flex flex-column">
2026
<ds-themed-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
21-
<span>{{getFileName(file)}}</span>
22-
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
27+
<span>{{getFileName(file)}}</span>
28+
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
2329
</ds-themed-file-download-link>
30+
</div>
2431
</ds-process-detail-field>
2532
</div>
2633

@@ -70,7 +77,7 @@ <h2 class="flex-grow-1">{{'process.detail.title' | translate:{
7077

7178
<ng-template #deleteModal >
7279

73-
<div *ngVar="(processRD$ | async)?.payload as process">
80+
<div *ngIf="(processRD$ | async)?.payload as process">
7481

7582
<div class="modal-header">
7683
<div>

src/app/process-page/detail/process-detail.component.spec.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser
3535
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
3636
import { NotificationsService } from '../../shared/notifications/notifications.service';
3737
import { getProcessListRoute } from '../process-page-routing.paths';
38+
import {ProcessStatus} from '../processes/process-status.model';
3839

3940
describe('ProcessDetailComponent', () => {
4041
let component: ProcessDetailComponent;
@@ -44,6 +45,7 @@ describe('ProcessDetailComponent', () => {
4445
let nameService: DSONameService;
4546
let bitstreamDataService: BitstreamDataService;
4647
let httpClient: HttpClient;
48+
let route: ActivatedRoute;
4749

4850
let process: Process;
4951
let fileName: string;
@@ -106,7 +108,8 @@ describe('ProcessDetailComponent', () => {
106108
});
107109
processService = jasmine.createSpyObj('processService', {
108110
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)),
109-
delete: createSuccessfulRemoteDataObject$(null)
111+
delete: createSuccessfulRemoteDataObject$(null),
112+
findById: createSuccessfulRemoteDataObject$(process),
110113
});
111114
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
112115
findByHref: createSuccessfulRemoteDataObject$(logBitstream)
@@ -127,6 +130,13 @@ describe('ProcessDetailComponent', () => {
127130
router = jasmine.createSpyObj('router', {
128131
navigateByUrl:{}
129132
});
133+
134+
route = jasmine.createSpyObj('route', {
135+
data: observableOf({ process: createSuccessfulRemoteDataObject(process) }),
136+
snapshot: {
137+
params: { id: process.processId }
138+
}
139+
});
130140
}
131141

132142
beforeEach(waitForAsync(() => {
@@ -263,4 +273,92 @@ describe('ProcessDetailComponent', () => {
263273
});
264274
});
265275

276+
describe('refresh counter', () => {
277+
const queryRefreshCounter = () => fixture.debugElement.query(By.css('.refresh-counter'));
278+
279+
describe('if process is completed', () => {
280+
beforeEach(() => {
281+
process.processStatus = ProcessStatus.COMPLETED;
282+
route.data = observableOf({process: createSuccessfulRemoteDataObject(process)});
283+
});
284+
285+
it('should not show', () => {
286+
spyOn(component, 'startRefreshTimer');
287+
288+
const refreshCounter = queryRefreshCounter();
289+
expect(refreshCounter).toBeNull();
290+
291+
expect(component.startRefreshTimer).not.toHaveBeenCalled();
292+
});
293+
});
294+
295+
describe('if process is not finished', () => {
296+
beforeEach(() => {
297+
process.processStatus = ProcessStatus.RUNNING;
298+
route.data = observableOf({process: createSuccessfulRemoteDataObject(process)});
299+
fixture.detectChanges();
300+
component.stopRefreshTimer();
301+
});
302+
303+
it('should call startRefreshTimer', () => {
304+
spyOn(component, 'startRefreshTimer');
305+
306+
component.ngOnInit();
307+
fixture.detectChanges(); // subscribe to process observable with async pipe
308+
309+
expect(component.startRefreshTimer).toHaveBeenCalled();
310+
});
311+
312+
it('should call refresh method every 5 seconds, until process is completed', fakeAsync(() => {
313+
spyOn(component, 'refresh');
314+
spyOn(component, 'stopRefreshTimer');
315+
316+
process.processStatus = ProcessStatus.COMPLETED;
317+
// set findbyId to return a completed process
318+
(processService.findById as jasmine.Spy).and.returnValue(observableOf(createSuccessfulRemoteDataObject(process)));
319+
320+
component.ngOnInit();
321+
fixture.detectChanges(); // subscribe to process observable with async pipe
322+
323+
expect(component.refresh).not.toHaveBeenCalled();
324+
325+
expect(component.refreshCounter$.value).toBe(0);
326+
327+
tick(1001); // 1 second + 1 ms by the setTimeout
328+
expect(component.refreshCounter$.value).toBe(5); // 5 - 0
329+
330+
tick(2001); // 2 seconds + 1 ms by the setTimeout
331+
expect(component.refreshCounter$.value).toBe(3); // 5 - 2
332+
333+
tick(2001); // 2 seconds + 1 ms by the setTimeout
334+
expect(component.refreshCounter$.value).toBe(1); // 3 - 2
335+
336+
tick(1001); // 1 second + 1 ms by the setTimeout
337+
expect(component.refreshCounter$.value).toBe(0); // 1 - 1
338+
339+
tick(1000); // 1 second
340+
341+
expect(component.refresh).toHaveBeenCalledTimes(1);
342+
expect(component.stopRefreshTimer).toHaveBeenCalled();
343+
344+
expect(component.refreshCounter$.value).toBe(0);
345+
346+
tick(1001); // 1 second + 1 ms by the setTimeout
347+
// startRefreshTimer not called again
348+
expect(component.refreshCounter$.value).toBe(0);
349+
350+
discardPeriodicTasks(); // discard any periodic tasks that have not yet executed
351+
}));
352+
353+
it('should show if refreshCounter is different from 0', () => {
354+
component.refreshCounter$.next(1);
355+
fixture.detectChanges();
356+
357+
const refreshCounter = queryRefreshCounter();
358+
expect(refreshCounter).not.toBeNull();
359+
});
360+
361+
});
362+
363+
});
266364
});

src/app/process-page/detail/process-detail.component.ts

Lines changed: 84 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { HttpClient } from '@angular/common/http';
2-
import { Component, NgZone, OnInit } from '@angular/core';
2+
import { Component, Inject, NgZone, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
33
import { ActivatedRoute, Router } from '@angular/router';
4-
import { BehaviorSubject, Observable } from 'rxjs';
4+
import { BehaviorSubject, interval, Observable, shareReplay, Subscription } from 'rxjs';
55
import { finalize, map, switchMap, take, tap } from 'rxjs/operators';
66
import { AuthService } from '../../core/auth/auth.service';
77
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
@@ -26,6 +26,8 @@ import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
2626
import { getProcessListRoute } from '../process-page-routing.paths';
2727
import { NotificationsService } from '../../shared/notifications/notifications.service';
2828
import { TranslateService } from '@ngx-translate/core';
29+
import { followLink } from '../../shared/utils/follow-link-config.model';
30+
import { isPlatformBrowser } from '@angular/common';
2931

3032
@Component({
3133
selector: 'ds-process-detail',
@@ -34,7 +36,7 @@ import { TranslateService } from '@ngx-translate/core';
3436
/**
3537
* A component displaying detailed information about a DSpace Process
3638
*/
37-
export class ProcessDetailComponent implements OnInit {
39+
export class ProcessDetailComponent implements OnInit, OnDestroy {
3840

3941
/**
4042
* The AlertType enumeration
@@ -65,48 +67,58 @@ export class ProcessDetailComponent implements OnInit {
6567
/**
6668
* Boolean on whether or not to show the output logs
6769
*/
68-
showOutputLogs;
70+
showOutputLogs = false;
6971
/**
7072
* When it's retrieving the output logs from backend, to show loading component
7173
*/
72-
retrievingOutputLogs$: BehaviorSubject<boolean>;
74+
retrievingOutputLogs$ = new BehaviorSubject<boolean>(false);
7375

7476
/**
7577
* Date format to use for start and end time of processes
7678
*/
7779
dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ';
7880

81+
refreshCounter$ = new BehaviorSubject(0);
82+
7983
/**
8084
* Reference to NgbModal
8185
*/
8286
protected modalRef: NgbModalRef;
8387

84-
constructor(protected route: ActivatedRoute,
85-
protected router: Router,
86-
protected processService: ProcessDataService,
87-
protected bitstreamDataService: BitstreamDataService,
88-
protected nameService: DSONameService,
89-
private zone: NgZone,
90-
protected authService: AuthService,
91-
protected http: HttpClient,
92-
protected modalService: NgbModal,
93-
protected notificationsService: NotificationsService,
94-
protected translateService: TranslateService
95-
) {
96-
}
88+
private refreshTimerSub?: Subscription;
89+
90+
constructor(
91+
@Inject(PLATFORM_ID) protected platformId: object,
92+
protected route: ActivatedRoute,
93+
protected router: Router,
94+
protected processService: ProcessDataService,
95+
protected bitstreamDataService: BitstreamDataService,
96+
protected nameService: DSONameService,
97+
private zone: NgZone,
98+
protected authService: AuthService,
99+
protected http: HttpClient,
100+
protected modalService: NgbModal,
101+
protected notificationsService: NotificationsService,
102+
protected translateService: TranslateService
103+
) {}
97104

98105
/**
99106
* Initialize component properties
100107
* Display a 404 if the process doesn't exist
101108
*/
102109
ngOnInit(): void {
103-
this.showOutputLogs = false;
104-
this.retrievingOutputLogs$ = new BehaviorSubject<boolean>(false);
105110
this.processRD$ = this.route.data.pipe(
106111
map((data) => {
112+
if (isPlatformBrowser(this.platformId)) {
113+
if (!this.isProcessFinished(data.process.payload)) {
114+
this.startRefreshTimer();
115+
}
116+
}
117+
107118
return data.process as RemoteData<Process>;
108119
}),
109-
redirectOn4xx(this.router, this.authService)
120+
redirectOn4xx(this.router, this.authService),
121+
shareReplay(1)
110122
);
111123

112124
this.filesRD$ = this.processRD$.pipe(
@@ -115,6 +127,53 @@ export class ProcessDetailComponent implements OnInit {
115127
);
116128
}
117129

130+
refresh() {
131+
this.processRD$ = this.processService.findById(
132+
this.route.snapshot.params.id,
133+
false,
134+
true,
135+
followLink('script')
136+
).pipe(
137+
getFirstSucceededRemoteData(),
138+
redirectOn4xx(this.router, this.authService),
139+
tap((processRemoteData: RemoteData<Process>) => {
140+
if (!this.isProcessFinished(processRemoteData.payload)) {
141+
this.startRefreshTimer();
142+
}
143+
}),
144+
shareReplay(1)
145+
);
146+
147+
this.filesRD$ = this.processRD$.pipe(
148+
getFirstSucceededRemoteDataPayload(),
149+
switchMap((process: Process) => this.processService.getFiles(process.processId))
150+
);
151+
}
152+
153+
startRefreshTimer() {
154+
this.refreshCounter$.next(0);
155+
156+
this.refreshTimerSub = interval(1000).subscribe(
157+
value => {
158+
if (value > 5) {
159+
setTimeout(() => {
160+
this.refresh();
161+
this.stopRefreshTimer();
162+
this.refreshCounter$.next(0);
163+
}, 1);
164+
} else {
165+
this.refreshCounter$.next(5 - value);
166+
}
167+
});
168+
}
169+
170+
stopRefreshTimer() {
171+
if (hasValue(this.refreshTimerSub)) {
172+
this.refreshTimerSub.unsubscribe();
173+
this.refreshTimerSub = undefined;
174+
}
175+
}
176+
118177
/**
119178
* Get the name of a bitstream
120179
* @param bitstream
@@ -210,11 +269,15 @@ export class ProcessDetailComponent implements OnInit {
210269
openDeleteModal(content) {
211270
this.modalRef = this.modalService.open(content);
212271
}
272+
213273
/**
214274
* Close the modal.
215275
*/
216276
closeModal() {
217277
this.modalRef.close();
218278
}
219279

280+
ngOnDestroy(): void {
281+
this.stopRefreshTimer();
282+
}
220283
}

0 commit comments

Comments
 (0)