Skip to content

Commit

Permalink
Merge pull request #2756 from atmire/process-admin-ui-redesign-8.0.0-…
Browse files Browse the repository at this point in the history
…next

Split processes overview page into sections
  • Loading branch information
tdonohue authored Feb 23, 2024
2 parents 45650c1 + ff4b4a6 commit fc6da94
Show file tree
Hide file tree
Showing 17 changed files with 926 additions and 285 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Input, OnDestroy } from '@angular/core';
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { ContentSource } from '../../../../core/shared/content-source.model';
import { ProcessDataService } from '../../../../core/data/processes/process-data.service';
Expand Down Expand Up @@ -29,7 +29,7 @@ import { ContentSourceSetSerializer } from '../../../../core/shared/content-sour
styleUrls: ['./collection-source-controls.component.scss'],
templateUrl: './collection-source-controls.component.html',
})
export class CollectionSourceControlsComponent implements OnDestroy {
export class CollectionSourceControlsComponent implements OnInit, OnDestroy {

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

contentSource$: Observable<ContentSource>;
private subs: Subscription[] = [];
private autoRefreshIDs: string[] = [];

testConfigRunning$ = new BehaviorSubject(false);
importRunning$ = new BehaviorSubject(false);
Expand Down Expand Up @@ -94,7 +95,10 @@ export class CollectionSourceControlsComponent implements OnDestroy {
}),
// filter out responses that aren't successful since the pinging of the process only needs to happen when the invocation was successful.
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
switchMap((rd) => this.processDataService.autoRefreshUntilCompletion(rd.payload.processId)),
switchMap((rd) => {
this.autoRefreshIDs.push(rd.payload.processId);
return this.processDataService.autoRefreshUntilCompletion(rd.payload.processId);
}),
map((rd) => rd.payload)
).subscribe((process: Process) => {
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) {
Expand Down Expand Up @@ -135,7 +139,10 @@ export class CollectionSourceControlsComponent implements OnDestroy {
}
}),
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
switchMap((rd) => this.processDataService.autoRefreshUntilCompletion(rd.payload.processId)),
switchMap((rd) => {
this.autoRefreshIDs.push(rd.payload.processId);
return this.processDataService.autoRefreshUntilCompletion(rd.payload.processId);
}),
map((rd) => rd.payload)
).subscribe((process) => {
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) {
Expand Down Expand Up @@ -170,7 +177,10 @@ export class CollectionSourceControlsComponent implements OnDestroy {
}
}),
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
switchMap((rd) => this.processDataService.autoRefreshUntilCompletion(rd.payload.processId)),
switchMap((rd) => {
this.autoRefreshIDs.push(rd.payload.processId);
return this.processDataService.autoRefreshUntilCompletion(rd.payload.processId);
}),
map((rd) => rd.payload)
).subscribe((process) => {
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) {
Expand All @@ -191,5 +201,9 @@ export class CollectionSourceControlsComponent implements OnDestroy {
sub.unsubscribe();
}
});

this.autoRefreshIDs.forEach((id) => {
this.processDataService.stopAutoRefreshing(id);
});
}
}
71 changes: 69 additions & 2 deletions src/app/core/data/processes/process-data.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { testFindAllDataImplementation } from '../base/find-all-data.spec';
import { ProcessDataService, TIMER_FACTORY } from './process-data.service';
import { testDeleteDataImplementation } from '../base/delete-data.spec';
import { waitForAsync, TestBed } from '@angular/core/testing';
import { waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { RequestService } from '../request.service';
import { RemoteData } from '../remote-data';
import { RequestEntryState } from '../request-entry-state.model';
Expand All @@ -23,6 +23,11 @@ import { DSOChangeAnalyzer } from '../dso-change-analyzer.service';
import { BitstreamFormatDataService } from '../bitstream-format-data.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TestScheduler } from 'rxjs/testing';
import { testSearchDataImplementation } from '../base/search-data.spec';
import { PaginatedList } from '../paginated-list.model';
import { FindListOptions } from '../find-list-options.model';
import { of } from 'rxjs';
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';

describe('ProcessDataService', () => {
let testScheduler;
Expand All @@ -36,9 +41,10 @@ describe('ProcessDataService', () => {
const initService = () => new ProcessDataService(null, null, null, null, null, null, null, null);
testFindAllDataImplementation(initService);
testDeleteDataImplementation(initService);
testSearchDataImplementation(initService);
});

let requestService;
let requestService = getMockRequestService();
let processDataService;
let remoteDataBuildService;

Expand Down Expand Up @@ -123,4 +129,65 @@ describe('ProcessDataService', () => {
expect(processDataService.invalidateByHref).toHaveBeenCalledTimes(1);
});
});

describe('autoRefreshingSearchBy', () => {
beforeEach(waitForAsync(() => {

TestBed.configureTestingModule({
imports: [],
providers: [
ProcessDataService,
{ provide: RequestService, useValue: requestService },
{ provide: RemoteDataBuildService, useValue: null },
{ provide: ObjectCacheService, useValue: null },
{ provide: ReducerManager, useValue: null },
{ provide: HALEndpointService, useValue: null },
{ provide: DSOChangeAnalyzer, useValue: null },
{ provide: BitstreamFormatDataService, useValue: null },
{ provide: NotificationsService, useValue: null },
{ provide: TIMER_FACTORY, useValue: mockTimer },
]
});

processDataService = TestBed.inject(ProcessDataService);
}));

it('should refresh after the specified interval', fakeAsync(() => {
const runningProcess = Object.assign(new Process(), {
_links: {
self: {
href: 'https://rest.api/processes/123'
}
}
});
runningProcess.processStatus = ProcessStatus.RUNNING;

const runningProcessPagination: PaginatedList<Process> = Object.assign(new PaginatedList(), {
page: [runningProcess],
_links: {
self: {
href: 'https://rest.api/processesList/456'
}
}
});

const runningProcessRD = new RemoteData(0, 0, 0, RequestEntryState.Success, null, runningProcessPagination);

spyOn(processDataService, 'searchBy').and.returnValue(
of(runningProcessRD)
);

expect(processDataService.searchBy).toHaveBeenCalledTimes(0);
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledTimes(0);

let sub = processDataService.autoRefreshingSearchBy('id', 'byProperty', new FindListOptions(), 200).subscribe();
expect(processDataService.searchBy).toHaveBeenCalledTimes(1);

tick(250);

expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledTimes(1);

sub.unsubscribe();
}));
});
});
86 changes: 78 additions & 8 deletions src/app/core/data/processes/process-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ObjectCacheService } from '../../cache/object-cache.service';
import { HALEndpointService } from '../../shared/hal-endpoint.service';
import { Process } from '../../../process-page/processes/process.model';
import { PROCESS } from '../../../process-page/processes/process.resource-type';
import { Observable } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { switchMap, filter, distinctUntilChanged, find } from 'rxjs/operators';
import { PaginatedList } from '../paginated-list.model';
import { Bitstream } from '../../shared/bitstream.model';
Expand All @@ -22,6 +22,7 @@ import { NoContent } from '../../shared/NoContent.model';
import { getAllCompletedRemoteData } from '../../shared/operators';
import { ProcessStatus } from 'src/app/process-page/processes/process-status.model';
import { hasValue } from '../../../shared/empty.util';
import { SearchData, SearchDataImpl } from '../base/search-data';

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

@Injectable()
@dataService(PROCESS)
export class ProcessDataService extends IdentifiableDataService<Process> implements FindAllData<Process>, DeleteData<Process> {
export class ProcessDataService extends IdentifiableDataService<Process> implements FindAllData<Process>, DeleteData<Process>, SearchData<Process> {

private findAllData: FindAllData<Process>;
private deleteData: DeleteData<Process>;
private searchData: SearchData<Process>;
protected activelyBeingPolled: Map<string, NodeJS.Timeout> = new Map();
protected subs: Map<string, Subscription> = new Map();

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

this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
}

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

/**
* @param searchMethod The search method for the Process
* @param options The FindListOptions object
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true.
* @param reRequestOnStale Whether the request should automatically be re-
* requested after the response becomes stale.
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should automatically be resolved.
* @return {Observable<RemoteData<PaginatedList<Process>>>}
* Return an observable that emits a paginated list of processes
*/
searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<Process>[]): Observable<RemoteData<PaginatedList<Process>>> {
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}

/**
* @param id The id for this auto-refreshing search. Used to stop
* auto-refreshing afterwards, and ensure we're not
* auto-refreshing the same thing multiple times.
* @param searchMethod The search method for the Process
* @param options The FindListOptions object
* @param pollingIntervalInMs The interval by which the search will be repeated
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should automatically be resolved.
* @return {Observable<RemoteData<PaginatedList<Process>>>}
* Return an observable that emits a paginated list of processes every interval
*/
autoRefreshingSearchBy(id: string, searchMethod: string, options?: FindListOptions, pollingIntervalInMs: number = 5000, ...linksToFollow: FollowLinkConfig<Process>[]): Observable<RemoteData<PaginatedList<Process>>> {

const result$ = this.searchBy(searchMethod, options, true, true, ...linksToFollow).pipe(
getAllCompletedRemoteData()
);

const sub = result$.pipe(
filter(() =>
!this.activelyBeingPolled.has(id)
)
).subscribe((processListRd: RemoteData<PaginatedList<Process>>) => {
this.clearCurrentTimeout(id);
const nextTimeout = this.timer(() => {
this.activelyBeingPolled.delete(id);
this.requestService.setStaleByHrefSubstring(processListRd.payload._links.self.href);
}, pollingIntervalInMs);

this.activelyBeingPolled.set(id, nextTimeout);
});

this.subs.set(id, sub);

return result$;
}

/**
* Stop auto-refreshing the request with the given id
* @param id the id of the request to stop automatically refreshing
*/
stopAutoRefreshing(id: string) {
this.clearCurrentTimeout(id);
if (hasValue(this.subs.get(id))) {
this.subs.get(id).unsubscribe();
this.subs.delete(id);
}
}

/**
* Delete an existing object on the server
* @param objectId The id of the object to be removed
Expand All @@ -135,14 +204,15 @@ export class ProcessDataService extends IdentifiableDataService<Process> impleme
}

/**
* Clear the timeout for the given process, if that timeout exists
* Clear the timeout for the given id, if that timeout exists
* @protected
*/
protected clearCurrentTimeout(processId: string): void {
const timeout = this.activelyBeingPolled.get(processId);
protected clearCurrentTimeout(id: string): void {
const timeout = this.activelyBeingPolled.get(id);
if (hasValue(timeout)) {
clearTimeout(timeout);
}
this.activelyBeingPolled.delete(id);
}

/**
Expand Down Expand Up @@ -185,15 +255,15 @@ export class ProcessDataService extends IdentifiableDataService<Process> impleme
}
});

this.subs.set(processId, sub);

// When the process completes create a one off subscription (the `find` completes the
// observable) that unsubscribes the previous one, removes the processId from the list of
// processes being polled and clears any running timeouts
process$.pipe(
find((processRD: RemoteData<Process>) => ProcessDataService.hasCompletedOrFailed(processRD.payload))
).subscribe(() => {
this.clearCurrentTimeout(processId);
this.activelyBeingPolled.delete(processId);
sub.unsubscribe();
this.stopAutoRefreshing(processId);
});

return process$.pipe(
Expand Down
18 changes: 15 additions & 3 deletions src/app/process-page/detail/process-detail.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HttpClient } from '@angular/common/http';
import { Component, Inject, NgZone, OnInit, PLATFORM_ID } from '@angular/core';
import { Component, Inject, NgZone, OnInit, PLATFORM_ID, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs';
import { finalize, map, switchMap, take, tap, find, startWith, filter } from 'rxjs/operators';
Expand Down Expand Up @@ -36,7 +36,7 @@ import { PROCESS_PAGE_FOLLOW_LINKS } from '../process-page.resolver';
/**
* A component displaying detailed information about a DSpace Process
*/
export class ProcessDetailComponent implements OnInit {
export class ProcessDetailComponent implements OnInit, OnDestroy {

/**
* The AlertType enumeration
Expand Down Expand Up @@ -82,6 +82,8 @@ export class ProcessDetailComponent implements OnInit {

isDeleting: boolean;

protected autoRefreshingID: string;

/**
* Reference to NgbModal
*/
Expand Down Expand Up @@ -110,7 +112,8 @@ export class ProcessDetailComponent implements OnInit {
this.processRD$ = this.route.data.pipe(
switchMap((data) => {
if (isPlatformBrowser(this.platformId)) {
return this.processService.autoRefreshUntilCompletion(this.route.snapshot.params.id, 5000, ...PROCESS_PAGE_FOLLOW_LINKS);
this.autoRefreshingID = this.route.snapshot.params.id;
return this.processService.autoRefreshUntilCompletion(this.autoRefreshingID, 5000, ...PROCESS_PAGE_FOLLOW_LINKS);
} else {
return [data.process as RemoteData<Process>];
}
Expand All @@ -131,6 +134,15 @@ export class ProcessDetailComponent implements OnInit {
);
}

/**
* Make sure the autoRefreshUntilCompletion is cleaned up properly
*/
ngOnDestroy() {
if (hasValue(this.autoRefreshingID)) {
this.processService.stopAutoRefreshing(this.autoRefreshingID);
}
}

/**
* Get the name of a bitstream
* @param bitstream
Expand Down
Loading

0 comments on commit fc6da94

Please sign in to comment.