Skip to content

Commit fc6da94

Browse files
authored
Merge pull request DSpace#2756 from atmire/process-admin-ui-redesign-8.0.0-next
Split processes overview page into sections
2 parents 45650c1 + ff4b4a6 commit fc6da94

17 files changed

Lines changed: 926 additions & 285 deletions

src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, Input, OnDestroy } from '@angular/core';
1+
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
22
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
33
import { ContentSource } from '../../../../core/shared/content-source.model';
44
import { ProcessDataService } from '../../../../core/data/processes/process-data.service';
@@ -29,7 +29,7 @@ import { ContentSourceSetSerializer } from '../../../../core/shared/content-sour
2929
styleUrls: ['./collection-source-controls.component.scss'],
3030
templateUrl: './collection-source-controls.component.html',
3131
})
32-
export class CollectionSourceControlsComponent implements OnDestroy {
32+
export class CollectionSourceControlsComponent implements OnInit, OnDestroy {
3333

3434
/**
3535
* Should the controls be enabled.
@@ -48,6 +48,7 @@ export class CollectionSourceControlsComponent implements OnDestroy {
4848

4949
contentSource$: Observable<ContentSource>;
5050
private subs: Subscription[] = [];
51+
private autoRefreshIDs: string[] = [];
5152

5253
testConfigRunning$ = new BehaviorSubject(false);
5354
importRunning$ = new BehaviorSubject(false);
@@ -94,7 +95,10 @@ export class CollectionSourceControlsComponent implements OnDestroy {
9495
}),
9596
// filter out responses that aren't successful since the pinging of the process only needs to happen when the invocation was successful.
9697
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
97-
switchMap((rd) => this.processDataService.autoRefreshUntilCompletion(rd.payload.processId)),
98+
switchMap((rd) => {
99+
this.autoRefreshIDs.push(rd.payload.processId);
100+
return this.processDataService.autoRefreshUntilCompletion(rd.payload.processId);
101+
}),
98102
map((rd) => rd.payload)
99103
).subscribe((process: Process) => {
100104
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) {
@@ -135,7 +139,10 @@ export class CollectionSourceControlsComponent implements OnDestroy {
135139
}
136140
}),
137141
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
138-
switchMap((rd) => this.processDataService.autoRefreshUntilCompletion(rd.payload.processId)),
142+
switchMap((rd) => {
143+
this.autoRefreshIDs.push(rd.payload.processId);
144+
return this.processDataService.autoRefreshUntilCompletion(rd.payload.processId);
145+
}),
139146
map((rd) => rd.payload)
140147
).subscribe((process) => {
141148
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) {
@@ -170,7 +177,10 @@ export class CollectionSourceControlsComponent implements OnDestroy {
170177
}
171178
}),
172179
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
173-
switchMap((rd) => this.processDataService.autoRefreshUntilCompletion(rd.payload.processId)),
180+
switchMap((rd) => {
181+
this.autoRefreshIDs.push(rd.payload.processId);
182+
return this.processDataService.autoRefreshUntilCompletion(rd.payload.processId);
183+
}),
174184
map((rd) => rd.payload)
175185
).subscribe((process) => {
176186
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) {
@@ -191,5 +201,9 @@ export class CollectionSourceControlsComponent implements OnDestroy {
191201
sub.unsubscribe();
192202
}
193203
});
204+
205+
this.autoRefreshIDs.forEach((id) => {
206+
this.processDataService.stopAutoRefreshing(id);
207+
});
194208
}
195209
}

src/app/core/data/processes/process-data.service.spec.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import { testFindAllDataImplementation } from '../base/find-all-data.spec';
1010
import { ProcessDataService, TIMER_FACTORY } from './process-data.service';
1111
import { testDeleteDataImplementation } from '../base/delete-data.spec';
12-
import { waitForAsync, TestBed } from '@angular/core/testing';
12+
import { waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing';
1313
import { RequestService } from '../request.service';
1414
import { RemoteData } from '../remote-data';
1515
import { RequestEntryState } from '../request-entry-state.model';
@@ -23,6 +23,11 @@ import { DSOChangeAnalyzer } from '../dso-change-analyzer.service';
2323
import { BitstreamFormatDataService } from '../bitstream-format-data.service';
2424
import { NotificationsService } from '../../../shared/notifications/notifications.service';
2525
import { TestScheduler } from 'rxjs/testing';
26+
import { testSearchDataImplementation } from '../base/search-data.spec';
27+
import { PaginatedList } from '../paginated-list.model';
28+
import { FindListOptions } from '../find-list-options.model';
29+
import { of } from 'rxjs';
30+
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
2631

2732
describe('ProcessDataService', () => {
2833
let testScheduler;
@@ -36,9 +41,10 @@ describe('ProcessDataService', () => {
3641
const initService = () => new ProcessDataService(null, null, null, null, null, null, null, null);
3742
testFindAllDataImplementation(initService);
3843
testDeleteDataImplementation(initService);
44+
testSearchDataImplementation(initService);
3945
});
4046

41-
let requestService;
47+
let requestService = getMockRequestService();
4248
let processDataService;
4349
let remoteDataBuildService;
4450

@@ -123,4 +129,65 @@ describe('ProcessDataService', () => {
123129
expect(processDataService.invalidateByHref).toHaveBeenCalledTimes(1);
124130
});
125131
});
132+
133+
describe('autoRefreshingSearchBy', () => {
134+
beforeEach(waitForAsync(() => {
135+
136+
TestBed.configureTestingModule({
137+
imports: [],
138+
providers: [
139+
ProcessDataService,
140+
{ provide: RequestService, useValue: requestService },
141+
{ provide: RemoteDataBuildService, useValue: null },
142+
{ provide: ObjectCacheService, useValue: null },
143+
{ provide: ReducerManager, useValue: null },
144+
{ provide: HALEndpointService, useValue: null },
145+
{ provide: DSOChangeAnalyzer, useValue: null },
146+
{ provide: BitstreamFormatDataService, useValue: null },
147+
{ provide: NotificationsService, useValue: null },
148+
{ provide: TIMER_FACTORY, useValue: mockTimer },
149+
]
150+
});
151+
152+
processDataService = TestBed.inject(ProcessDataService);
153+
}));
154+
155+
it('should refresh after the specified interval', fakeAsync(() => {
156+
const runningProcess = Object.assign(new Process(), {
157+
_links: {
158+
self: {
159+
href: 'https://rest.api/processes/123'
160+
}
161+
}
162+
});
163+
runningProcess.processStatus = ProcessStatus.RUNNING;
164+
165+
const runningProcessPagination: PaginatedList<Process> = Object.assign(new PaginatedList(), {
166+
page: [runningProcess],
167+
_links: {
168+
self: {
169+
href: 'https://rest.api/processesList/456'
170+
}
171+
}
172+
});
173+
174+
const runningProcessRD = new RemoteData(0, 0, 0, RequestEntryState.Success, null, runningProcessPagination);
175+
176+
spyOn(processDataService, 'searchBy').and.returnValue(
177+
of(runningProcessRD)
178+
);
179+
180+
expect(processDataService.searchBy).toHaveBeenCalledTimes(0);
181+
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledTimes(0);
182+
183+
let sub = processDataService.autoRefreshingSearchBy('id', 'byProperty', new FindListOptions(), 200).subscribe();
184+
expect(processDataService.searchBy).toHaveBeenCalledTimes(1);
185+
186+
tick(250);
187+
188+
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledTimes(1);
189+
190+
sub.unsubscribe();
191+
}));
192+
});
126193
});

src/app/core/data/processes/process-data.service.ts

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ObjectCacheService } from '../../cache/object-cache.service';
55
import { HALEndpointService } from '../../shared/hal-endpoint.service';
66
import { Process } from '../../../process-page/processes/process.model';
77
import { PROCESS } from '../../../process-page/processes/process.resource-type';
8-
import { Observable } from 'rxjs';
8+
import { Observable, Subscription } from 'rxjs';
99
import { switchMap, filter, distinctUntilChanged, find } from 'rxjs/operators';
1010
import { PaginatedList } from '../paginated-list.model';
1111
import { Bitstream } from '../../shared/bitstream.model';
@@ -22,6 +22,7 @@ import { NoContent } from '../../shared/NoContent.model';
2222
import { getAllCompletedRemoteData } from '../../shared/operators';
2323
import { ProcessStatus } from 'src/app/process-page/processes/process-status.model';
2424
import { hasValue } from '../../../shared/empty.util';
25+
import { SearchData, SearchDataImpl } from '../base/search-data';
2526

2627
/**
2728
* Create an InjectionToken for the default JS setTimeout function, purely so we can mock it during
@@ -34,11 +35,13 @@ export const TIMER_FACTORY = new InjectionToken<(callback: (...args: any[]) => v
3435

3536
@Injectable()
3637
@dataService(PROCESS)
37-
export class ProcessDataService extends IdentifiableDataService<Process> implements FindAllData<Process>, DeleteData<Process> {
38+
export class ProcessDataService extends IdentifiableDataService<Process> implements FindAllData<Process>, DeleteData<Process>, SearchData<Process> {
3839

3940
private findAllData: FindAllData<Process>;
4041
private deleteData: DeleteData<Process>;
42+
private searchData: SearchData<Process>;
4143
protected activelyBeingPolled: Map<string, NodeJS.Timeout> = new Map();
44+
protected subs: Map<string, Subscription> = new Map();
4245

4346
constructor(
4447
protected requestService: RequestService,
@@ -54,6 +57,7 @@ export class ProcessDataService extends IdentifiableDataService<Process> impleme
5457

5558
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
5659
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
60+
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
5761
}
5862

5963
/**
@@ -109,6 +113,71 @@ export class ProcessDataService extends IdentifiableDataService<Process> impleme
109113
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
110114
}
111115

116+
/**
117+
* @param searchMethod The search method for the Process
118+
* @param options The FindListOptions object
119+
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
120+
* no valid cached version. Defaults to true.
121+
* @param reRequestOnStale Whether the request should automatically be re-
122+
* requested after the response becomes stale.
123+
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
124+
* {@link HALLink}s should automatically be resolved.
125+
* @return {Observable<RemoteData<PaginatedList<Process>>>}
126+
* Return an observable that emits a paginated list of processes
127+
*/
128+
searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<Process>[]): Observable<RemoteData<PaginatedList<Process>>> {
129+
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
130+
}
131+
132+
/**
133+
* @param id The id for this auto-refreshing search. Used to stop
134+
* auto-refreshing afterwards, and ensure we're not
135+
* auto-refreshing the same thing multiple times.
136+
* @param searchMethod The search method for the Process
137+
* @param options The FindListOptions object
138+
* @param pollingIntervalInMs The interval by which the search will be repeated
139+
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
140+
* {@link HALLink}s should automatically be resolved.
141+
* @return {Observable<RemoteData<PaginatedList<Process>>>}
142+
* Return an observable that emits a paginated list of processes every interval
143+
*/
144+
autoRefreshingSearchBy(id: string, searchMethod: string, options?: FindListOptions, pollingIntervalInMs: number = 5000, ...linksToFollow: FollowLinkConfig<Process>[]): Observable<RemoteData<PaginatedList<Process>>> {
145+
146+
const result$ = this.searchBy(searchMethod, options, true, true, ...linksToFollow).pipe(
147+
getAllCompletedRemoteData()
148+
);
149+
150+
const sub = result$.pipe(
151+
filter(() =>
152+
!this.activelyBeingPolled.has(id)
153+
)
154+
).subscribe((processListRd: RemoteData<PaginatedList<Process>>) => {
155+
this.clearCurrentTimeout(id);
156+
const nextTimeout = this.timer(() => {
157+
this.activelyBeingPolled.delete(id);
158+
this.requestService.setStaleByHrefSubstring(processListRd.payload._links.self.href);
159+
}, pollingIntervalInMs);
160+
161+
this.activelyBeingPolled.set(id, nextTimeout);
162+
});
163+
164+
this.subs.set(id, sub);
165+
166+
return result$;
167+
}
168+
169+
/**
170+
* Stop auto-refreshing the request with the given id
171+
* @param id the id of the request to stop automatically refreshing
172+
*/
173+
stopAutoRefreshing(id: string) {
174+
this.clearCurrentTimeout(id);
175+
if (hasValue(this.subs.get(id))) {
176+
this.subs.get(id).unsubscribe();
177+
this.subs.delete(id);
178+
}
179+
}
180+
112181
/**
113182
* Delete an existing object on the server
114183
* @param objectId The id of the object to be removed
@@ -135,14 +204,15 @@ export class ProcessDataService extends IdentifiableDataService<Process> impleme
135204
}
136205

137206
/**
138-
* Clear the timeout for the given process, if that timeout exists
207+
* Clear the timeout for the given id, if that timeout exists
139208
* @protected
140209
*/
141-
protected clearCurrentTimeout(processId: string): void {
142-
const timeout = this.activelyBeingPolled.get(processId);
210+
protected clearCurrentTimeout(id: string): void {
211+
const timeout = this.activelyBeingPolled.get(id);
143212
if (hasValue(timeout)) {
144213
clearTimeout(timeout);
145214
}
215+
this.activelyBeingPolled.delete(id);
146216
}
147217

148218
/**
@@ -185,15 +255,15 @@ export class ProcessDataService extends IdentifiableDataService<Process> impleme
185255
}
186256
});
187257

258+
this.subs.set(processId, sub);
259+
188260
// When the process completes create a one off subscription (the `find` completes the
189261
// observable) that unsubscribes the previous one, removes the processId from the list of
190262
// processes being polled and clears any running timeouts
191263
process$.pipe(
192264
find((processRD: RemoteData<Process>) => ProcessDataService.hasCompletedOrFailed(processRD.payload))
193265
).subscribe(() => {
194-
this.clearCurrentTimeout(processId);
195-
this.activelyBeingPolled.delete(processId);
196-
sub.unsubscribe();
266+
this.stopAutoRefreshing(processId);
197267
});
198268

199269
return process$.pipe(

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { HttpClient } from '@angular/common/http';
2-
import { Component, Inject, NgZone, OnInit, PLATFORM_ID } from '@angular/core';
2+
import { Component, Inject, NgZone, OnInit, PLATFORM_ID, OnDestroy } from '@angular/core';
33
import { ActivatedRoute, Router } from '@angular/router';
44
import { BehaviorSubject, Observable } from 'rxjs';
55
import { finalize, map, switchMap, take, tap, find, startWith, filter } from 'rxjs/operators';
@@ -36,7 +36,7 @@ import { PROCESS_PAGE_FOLLOW_LINKS } from '../process-page.resolver';
3636
/**
3737
* A component displaying detailed information about a DSpace Process
3838
*/
39-
export class ProcessDetailComponent implements OnInit {
39+
export class ProcessDetailComponent implements OnInit, OnDestroy {
4040

4141
/**
4242
* The AlertType enumeration
@@ -82,6 +82,8 @@ export class ProcessDetailComponent implements OnInit {
8282

8383
isDeleting: boolean;
8484

85+
protected autoRefreshingID: string;
86+
8587
/**
8688
* Reference to NgbModal
8789
*/
@@ -110,7 +112,8 @@ export class ProcessDetailComponent implements OnInit {
110112
this.processRD$ = this.route.data.pipe(
111113
switchMap((data) => {
112114
if (isPlatformBrowser(this.platformId)) {
113-
return this.processService.autoRefreshUntilCompletion(this.route.snapshot.params.id, 5000, ...PROCESS_PAGE_FOLLOW_LINKS);
115+
this.autoRefreshingID = this.route.snapshot.params.id;
116+
return this.processService.autoRefreshUntilCompletion(this.autoRefreshingID, 5000, ...PROCESS_PAGE_FOLLOW_LINKS);
114117
} else {
115118
return [data.process as RemoteData<Process>];
116119
}
@@ -131,6 +134,15 @@ export class ProcessDetailComponent implements OnInit {
131134
);
132135
}
133136

137+
/**
138+
* Make sure the autoRefreshUntilCompletion is cleaned up properly
139+
*/
140+
ngOnDestroy() {
141+
if (hasValue(this.autoRefreshingID)) {
142+
this.processService.stopAutoRefreshing(this.autoRefreshingID);
143+
}
144+
}
145+
134146
/**
135147
* Get the name of a bitstream
136148
* @param bitstream

0 commit comments

Comments
 (0)