Skip to content

Commit c2298f6

Browse files
authored
Merge pull request #636 from atmire/scripts-processes
Scripts & processes admin UI
2 parents 5e191ff + 7cfa0f1 commit c2298f6

90 files changed

Lines changed: 3136 additions & 41 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
"moment": "^2.22.1",
102102
"morgan": "^1.9.1",
103103
"ng-mocks": "^8.1.0",
104-
"ng2-file-upload": "1.2.1",
104+
"ng2-file-upload": "1.4.0",
105105
"ng2-nouislider": "^1.8.2",
106106
"ngx-bootstrap": "^5.3.2",
107107
"ngx-infinite-scroll": "6.0.1",

src/app/+admin/admin-sidebar/admin-sidebar.component.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,17 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
145145
this.modalService.open(CreateItemParentSelectorComponent);
146146
}
147147
} as OnClickMenuItemModel,
148-
// model: {
149-
// type: MenuItemType.LINK,
150-
// text: 'menu.section.new_item',
151-
// link: '/submit'
152-
// } as LinkMenuItemModel,
148+
},
149+
{
150+
id: 'new_process',
151+
parentID: 'new',
152+
active: false,
153+
visible: true,
154+
model: {
155+
type: MenuItemType.LINK,
156+
text: 'menu.section.new_process',
157+
link: '/processes/new'
158+
} as LinkMenuItemModel,
153159
},
154160
{
155161
id: 'new_item_version',
@@ -439,6 +445,19 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
439445
icon: 'cogs',
440446
index: 9
441447
},
448+
/* Processes */
449+
{
450+
id: 'processes',
451+
active: false,
452+
visible: true,
453+
model: {
454+
type: MenuItemType.LINK,
455+
text: 'menu.section.processes',
456+
link: '/processes'
457+
} as LinkMenuItemModel,
458+
icon: 'terminal',
459+
index: 10
460+
},
442461
/* Workflow */
443462
{
444463
id: 'workflow',

src/app/app-routing.module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export function getDSOPath(dso: DSpaceObject): string {
114114
path: PROFILE_MODULE_PATH,
115115
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
116116
},
117+
{ path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard] },
117118
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
118119
],
119120
{

src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,38 @@
11
import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver';
2+
import { URLCombiner } from '../url-combiner/url-combiner';
23

34
describe('I18nBreadcrumbResolver', () => {
45
describe('resolve', () => {
56
let resolver: I18nBreadcrumbResolver;
67
let i18nBreadcrumbService: any;
78
let i18nKey: string;
8-
let path: string;
9+
let route: any;
10+
let parentSegment;
11+
let segment;
12+
let expectedPath;
913
beforeEach(() => {
1014
i18nKey = 'example.key';
11-
path = 'rest.com/path/to/breadcrumb';
15+
parentSegment = 'path';
16+
segment = 'breadcrumb';
17+
route = {
18+
data: { breadcrumbKey: i18nKey },
19+
routeConfig: {
20+
path: segment
21+
},
22+
parent: {
23+
routeConfig: {
24+
path: parentSegment
25+
}
26+
} as any
27+
};
28+
expectedPath = new URLCombiner(parentSegment, segment).toString();
1229
i18nBreadcrumbService = {};
1330
resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService);
1431
});
1532

1633
it('should resolve the breadcrumb config', () => {
17-
const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path], pathFromRoot: [{ url: [path] }] } as any, {} as any);
18-
const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: path };
34+
const resolvedConfig = resolver.resolve(route, {} as any);
35+
const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: expectedPath };
1936
expect(resolvedConfig).toEqual(expectedConfig);
2037
});
2138

src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
33
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
44
import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service';
55
import { hasNoValue } from '../../shared/empty.util';
6+
import { currentPathFromSnapshot } from '../../shared/utils/route.utils';
67

78
/**
89
* The class that resolves a BreadcrumbConfig object with an i18n key string for a route
@@ -25,17 +26,7 @@ export class I18nBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>>
2526
if (hasNoValue(key)) {
2627
throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data')
2728
}
28-
const fullPath = this.getResolvedUrl(route);
29+
const fullPath = currentPathFromSnapshot(route);
2930
return { provider: this.breadcrumbService, key: key, url: fullPath };
3031
}
31-
32-
/**
33-
* Resolve the full URL of an ActivatedRouteSnapshot
34-
* @param route
35-
*/
36-
getResolvedUrl(route: ActivatedRouteSnapshot): string {
37-
return route.pathFromRoot
38-
.map((v) => v.url.map((segment) => segment.toString()).join('/'))
39-
.join('/');
40-
}
4132
}

src/app/core/core.module.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { CommonModule } from '@angular/common';
22
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
33
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
4-
54
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
65
import { EffectsModule } from '@ngrx/effects';
76

@@ -138,6 +137,11 @@ import { VersionDataService } from './data/version-data.service';
138137
import { VersionHistoryDataService } from './data/version-history-data.service';
139138
import { Version } from './shared/version.model';
140139
import { VersionHistory } from './shared/version-history.model';
140+
import { Script } from '../process-page/scripts/script.model';
141+
import { Process } from '../process-page/processes/process.model';
142+
import { ProcessDataService } from './data/processes/process-data.service';
143+
import { ScriptDataService } from './data/processes/script-data.service';
144+
import { ProcessFilesResponseParsingService } from './data/process-files-response-parsing.service';
141145
import { WorkflowActionDataService } from './data/workflow-action-data.service';
142146
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
143147
import { Registration } from './shared/registration.model';
@@ -258,6 +262,9 @@ const PROVIDERS = [
258262
LicenseDataService,
259263
ItemTypeDataService,
260264
WorkflowActionDataService,
265+
ProcessDataService,
266+
ScriptDataService,
267+
ProcessFilesResponseParsingService,
261268
MetadataSchemaDataService,
262269
MetadataFieldDataService,
263270
TokenResponseParsingService,
@@ -309,6 +316,8 @@ export const models =
309316
ItemType,
310317
ExternalSource,
311318
ExternalSourceEntry,
319+
Script,
320+
Process,
312321
Version,
313322
VersionHistory,
314323
WorkflowAction,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { ResponseParsingService } from './parsing.service';
2+
import { RestRequest } from './request.models';
3+
import { GenericSuccessResponse, RestResponse } from '../cache/response.models';
4+
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
5+
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
6+
import { PaginatedList } from './paginated-list';
7+
import { PageInfo } from '../shared/page-info.model';
8+
import { Injectable } from '@angular/core';
9+
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
10+
import { Bitstream } from '../shared/bitstream.model';
11+
12+
@Injectable()
13+
/**
14+
* A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a GenericSuccessResponse
15+
* containing a PaginatedList of a process's output files
16+
*/
17+
export class ProcessFilesResponseParsingService implements ResponseParsingService {
18+
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
19+
const payload = data.payload;
20+
21+
let page;
22+
if (isNotEmpty(payload._embedded) && isNotEmpty(Object.keys(payload._embedded))) {
23+
const bitstreams = new DSpaceSerializer(Bitstream).deserializeArray(payload._embedded[Object.keys(payload._embedded)[0]]);
24+
25+
if (isNotEmpty(bitstreams)) {
26+
page = new PaginatedList(Object.assign(new PageInfo(), {
27+
elementsPerPage: bitstreams.length,
28+
totalElements: bitstreams.length,
29+
totalPages: 1,
30+
currentPage: 1
31+
}), bitstreams);
32+
}
33+
}
34+
35+
if (isEmpty(page)) {
36+
page = new PaginatedList(new PageInfo(), []);
37+
}
38+
39+
return new GenericSuccessResponse(page, data.statusCode, data.statusText);
40+
}
41+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Injectable } from '@angular/core';
2+
import { DataService } from '../data.service';
3+
import { RequestService } from '../request.service';
4+
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
5+
import { Store } from '@ngrx/store';
6+
import { CoreState } from '../../core.reducers';
7+
import { ObjectCacheService } from '../../cache/object-cache.service';
8+
import { HALEndpointService } from '../../shared/hal-endpoint.service';
9+
import { NotificationsService } from '../../../shared/notifications/notifications.service';
10+
import { HttpClient } from '@angular/common/http';
11+
import { DefaultChangeAnalyzer } from '../default-change-analyzer.service';
12+
import { Process } from '../../../process-page/processes/process.model';
13+
import { dataService } from '../../cache/builders/build-decorators';
14+
import { PROCESS } from '../../../process-page/processes/process.resource-type';
15+
import { Observable } from 'rxjs/internal/Observable';
16+
import { map, switchMap } from 'rxjs/operators';
17+
import { ProcessFilesRequest, RestRequest } from '../request.models';
18+
import { configureRequest, filterSuccessfulResponses } from '../../shared/operators';
19+
import { GenericSuccessResponse } from '../../cache/response.models';
20+
import { PaginatedList } from '../paginated-list';
21+
import { Bitstream } from '../../shared/bitstream.model';
22+
import { RemoteData } from '../remote-data';
23+
24+
@Injectable()
25+
@dataService(PROCESS)
26+
export class ProcessDataService extends DataService<Process> {
27+
protected linkPath = 'processes';
28+
29+
constructor(
30+
protected requestService: RequestService,
31+
protected rdbService: RemoteDataBuildService,
32+
protected store: Store<CoreState>,
33+
protected objectCache: ObjectCacheService,
34+
protected halService: HALEndpointService,
35+
protected notificationsService: NotificationsService,
36+
protected http: HttpClient,
37+
protected comparator: DefaultChangeAnalyzer<Process>) {
38+
super();
39+
}
40+
41+
/**
42+
* Get the endpoint for a process his files
43+
* @param processId The ID of the process
44+
*/
45+
getFilesEndpoint(processId: string): Observable<string> {
46+
return this.getBrowseEndpoint().pipe(
47+
switchMap((href) => this.halService.getEndpoint('files', `${href}/${processId}`))
48+
);
49+
}
50+
51+
/**
52+
* Get a process his output files
53+
* @param processId The ID of the process
54+
*/
55+
getFiles(processId: string): Observable<RemoteData<PaginatedList<Bitstream>>> {
56+
const request$ = this.getFilesEndpoint(processId).pipe(
57+
map((href) => new ProcessFilesRequest(this.requestService.generateRequestId(), href)),
58+
configureRequest(this.requestService)
59+
);
60+
const requestEntry$ = request$.pipe(
61+
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
62+
);
63+
const payload$ = requestEntry$.pipe(
64+
filterSuccessfulResponses(),
65+
map((response: GenericSuccessResponse<PaginatedList<Bitstream>>) => response.payload)
66+
);
67+
68+
return this.rdbService.toRemoteDataObservable(requestEntry$, payload$);
69+
}
70+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Injectable } from '@angular/core';
2+
import { DataService } from '../data.service';
3+
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
4+
import { Store } from '@ngrx/store';
5+
import { CoreState } from '../../core.reducers';
6+
import { ObjectCacheService } from '../../cache/object-cache.service';
7+
import { HALEndpointService } from '../../shared/hal-endpoint.service';
8+
import { NotificationsService } from '../../../shared/notifications/notifications.service';
9+
import { HttpClient } from '@angular/common/http';
10+
import { DefaultChangeAnalyzer } from '../default-change-analyzer.service';
11+
import { Script } from '../../../process-page/scripts/script.model';
12+
import { ProcessParameter } from '../../../process-page/processes/process-parameter.model';
13+
import { find, map, switchMap } from 'rxjs/operators';
14+
import { URLCombiner } from '../../url-combiner/url-combiner';
15+
import { MultipartPostRequest, RestRequest } from '../request.models';
16+
import { RequestService } from '../request.service';
17+
import { Observable } from 'rxjs';
18+
import { RequestEntry } from '../request.reducer';
19+
import { dataService } from '../../cache/builders/build-decorators';
20+
import { SCRIPT } from '../../../process-page/scripts/script.resource-type';
21+
22+
@Injectable()
23+
@dataService(SCRIPT)
24+
export class ScriptDataService extends DataService<Script> {
25+
protected linkPath = 'scripts';
26+
27+
constructor(
28+
protected requestService: RequestService,
29+
protected rdbService: RemoteDataBuildService,
30+
protected store: Store<CoreState>,
31+
protected objectCache: ObjectCacheService,
32+
protected halService: HALEndpointService,
33+
protected notificationsService: NotificationsService,
34+
protected http: HttpClient,
35+
protected comparator: DefaultChangeAnalyzer<Script>) {
36+
super();
37+
}
38+
39+
public invoke(scriptName: string, parameters: ProcessParameter[], files: File[]): Observable<RequestEntry> {
40+
const requestId = this.requestService.generateRequestId();
41+
return this.getBrowseEndpoint().pipe(
42+
map((endpoint: string) => new URLCombiner(endpoint, scriptName, 'processes').toString()),
43+
map((endpoint: string) => {
44+
const body = this.getInvocationFormData(parameters, files);
45+
return new MultipartPostRequest(requestId, endpoint, body)
46+
}),
47+
map((request: RestRequest) => this.requestService.configure(request)),
48+
switchMap(() => this.requestService.getByUUID(requestId)),
49+
find((request: RequestEntry) => request.completed)
50+
);
51+
}
52+
53+
private getInvocationFormData(parameters: ProcessParameter[], files: File[]): FormData {
54+
const form: FormData = new FormData();
55+
form.set('properties', JSON.stringify(parameters));
56+
files.forEach((file: File) => {
57+
form.append('file', file);
58+
});
59+
return form;
60+
}
61+
}

src/app/core/data/request.effects.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,7 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.
1111

1212
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
1313
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
14-
import {
15-
RequestActionTypes,
16-
RequestCompleteAction,
17-
RequestExecuteAction,
18-
ResetResponseTimestampsAction
19-
} from './request.actions';
14+
import { RequestActionTypes, RequestCompleteAction, RequestExecuteAction, ResetResponseTimestampsAction } from './request.actions';
2015
import { RequestError, RestRequest } from './request.models';
2116
import { RequestEntry } from './request.reducer';
2217
import { RequestService } from './request.service';
@@ -42,12 +37,12 @@ export class RequestEffects {
4237
filter((entry: RequestEntry) => hasValue(entry)),
4338
map((entry: RequestEntry) => entry.request),
4439
flatMap((request: RestRequest) => {
45-
let body;
46-
if (isNotEmpty(request.body)) {
40+
let body = request.body;
41+
if (isNotEmpty(request.body) && !request.isMultipart) {
4742
const serializer = new DSpaceSerializer(getClassForType(request.body.type));
4843
body = serializer.serialize(request.body);
4944
}
50-
return this.restApi.request(request.method, request.href, body, request.options).pipe(
45+
return this.restApi.request(request.method, request.href, body, request.options, request.isMultipart).pipe(
5146
map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)),
5247
addToResponseCacheAndCompleteAction(request),
5348
catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe(

0 commit comments

Comments
 (0)