83707: Control collection harvest from the UI

This commit is contained in:
Yana De Pauw
2021-09-23 15:47:43 +02:00
parent 706cc47197
commit 100166023a
8 changed files with 287 additions and 57 deletions

View File

@@ -0,0 +1,35 @@
<div *ngVar="(contentSource$ |async) as contentSource">
<div class="container-fluid" *ngIf="shouldShow">
<h4>{{ 'collection.source.controls.head' | translate }}</h4>
<div>
<span class="font-weight-bold">{{'collection.source.controls.harvest.status' | translate}}</span>
<span>{{contentSource?.harvestStatus}}</span>
</div>
<div>
<span class="font-weight-bold">{{'collection.source.controls.harvest.start' | translate}}</span>
<span>{{contentSource?.harvestStartTime ? contentSource?.harvestStartTime : 'collection.source.controls.harvest.no-information'|translate }}</span>
</div>
<div>
<span class="font-weight-bold">{{'collection.source.controls.harvest.last' | translate}}</span>
<span>{{contentSource?.lastHarvested ? contentSource?.lastHarvested : 'collection.source.controls.harvest.no-information'|translate }}</span>
</div>
<button class=" btn btn-secondary"
[disabled]="!(isEnabled)"
(click)="testConfiguration(contentSource)">
<span class="d-none d-sm-inline">{{'collection.source.controls.test.submit' | translate}}</span>
</button>
<button class="btn btn-primary"
[disabled]="!(isEnabled)"
(click)="importNow()">
<span class="d-none d-sm-inline">{{'collection.source.controls.import.submit' | translate}}</span>
</button>
<button class="btn btn-primary"
[disabled]="!(isEnabled)"
(click)="resetAndReimport()">
<span class="d-none d-sm-inline">&nbsp;{{'collection.source.controls.reset.submit' | translate}}</span>
</button>
</div>
</div>

View File

@@ -0,0 +1,150 @@
import { Component, Input, OnDestroy } 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';
import {
getAllCompletedRemoteData,
getAllSucceededRemoteDataPayload,
getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload
} from '../../../../core/shared/operators';
import { filter, map, switchMap, tap } from 'rxjs/operators';
import { hasValue, hasValueOperator } from '../../../../shared/empty.util';
import { ProcessStatus } from '../../../../process-page/processes/process-status.model';
import { Subscription } from 'rxjs/internal/Subscription';
import { RequestService } from '../../../../core/data/request.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { Collection } from '../../../../core/shared/collection.model';
import { CollectionDataService } from '../../../../core/data/collection-data.service';
import { Observable } from 'rxjs/internal/Observable';
import { Process } from '../../../../process-page/processes/process.model';
import { TranslateService } from '@ngx-translate/core';
import { HttpClient } from '@angular/common/http';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
@Component({
selector: 'ds-collection-source-controls',
templateUrl: './collection-source-controls.component.html',
})
export class CollectionSourceControlsComponent implements OnDestroy {
@Input() isEnabled: boolean;
@Input() collection: Collection;
@Input() shouldShow: boolean;
contentSource$: Observable<ContentSource>;
private subs: Subscription[] = [];
constructor(private scriptDataService: ScriptDataService,
private processDataService: ProcessDataService,
private requestService: RequestService,
private notificationsService: NotificationsService,
private collectionService: CollectionDataService,
private translateService: TranslateService,
private httpClient: HttpClient,
private bitstreamService: BitstreamDataService
) {
}
ngOnInit() {
this.contentSource$ = this.collectionService.findByHref(this.collection._links.self.href, false).pipe(
getAllSucceededRemoteDataPayload(),
switchMap((collection) => this.collectionService.getContentSource(collection.uuid, false)),
getAllSucceededRemoteDataPayload()
);
}
testConfiguration(contentSource) {
this.subs.push(this.scriptDataService.invoke('harvest', [
{name: '-g', value: null},
{name: '-a', value: contentSource.oaiSource},
{name: '-i', value: contentSource.oaiSetId},
], []).pipe(
getFirstCompletedRemoteData(),
tap((rd) => {
if (rd.hasFailed) {
this.notificationsService.error(this.translateService.get('collection.source.controls.test.submit.error'));
}
}),
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
switchMap((rd) => this.processDataService.findById(rd.payload.processId, false)),
getAllCompletedRemoteData(),
filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)),
map((rd) => rd.payload),
hasValueOperator(),
).subscribe((process: Process) => {
if (process.processStatus.toString() !== ProcessStatus[ProcessStatus.COMPLETED].toString() &&
process.processStatus.toString() !== ProcessStatus[ProcessStatus.FAILED].toString()) {
setTimeout(() => {
this.requestService.setStaleByHrefSubstring(process._links.self.href);
}, 5000);
}
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) {
this.notificationsService.error(this.translateService.get('collection.source.controls.test.failed'));
}
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) {
this.bitstreamService.findByHref(process._links.output.href, false).pipe(getFirstSucceededRemoteDataPayload()).subscribe((bitstream) => {
this.httpClient.get(bitstream._links.content.href, {responseType: 'text'}).subscribe((data: any) => {
const output = data.replaceAll(new RegExp('.*\\@(.*)', 'g'), '$1')
.replaceAll('The script has started', '')
.replaceAll('The script has completed', '');
this.notificationsService.success(this.translateService.get('collection.source.controls.test.completed'), output);
});
});
}
}
));
}
importNow() {
this.subs.push(this.scriptDataService.invoke('harvest', [
{name: '-r', value: null},
{name: '-e', value: 'dspacedemo+admin@gmail.com'},
{name: '-c', value: this.collection.uuid},
], [])
.pipe(
getFirstCompletedRemoteData(),
tap((rd) => {
if (rd.hasFailed) {
this.notificationsService.error(this.translateService.get('collection.source.controls.test.submit.error'));
}
}),
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
switchMap((rd) => this.processDataService.findById(rd.payload.processId, false)),
getAllCompletedRemoteData(),
filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)),
map((rd) => rd.payload),
hasValueOperator(),
).subscribe((process) => {
if (process.processStatus.toString() !== ProcessStatus[ProcessStatus.COMPLETED].toString() &&
process.processStatus.toString() !== ProcessStatus[ProcessStatus.FAILED].toString()) {
setTimeout(() => {
this.requestService.setStaleByHrefSubstring(process._links.self.href);
this.requestService.setStaleByHrefSubstring(this.collection._links.self.href);
}, 5000);
}
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) {
this.notificationsService.error(this.translateService.get('collection.source.controls.import.failed'));
}
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) {
this.notificationsService.success(this.translateService.get('collection.source.controls.import.completed'));
setTimeout(() => {
this.requestService.setStaleByHrefSubstring(this.collection._links.self.href);
}, 5000);
}
}
));
}
resetAndReimport() {
// TODO implement when a single option is present
}
ngOnDestroy(): void {
this.subs.forEach((sub) => {
if (hasValue(sub)) {
sub.unsubscribe();
}
});
}
}

View File

@@ -11,7 +11,8 @@
class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span>
</button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
<button class="btn btn-primary"
[disabled]="!(hasChanges() | async) || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
(click)="onSubmit()"><i
class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>
@@ -19,12 +20,15 @@
</div>
<h4>{{ 'collection.edit.tabs.source.head' | translate }}</h4>
<div *ngIf="contentSource" class="form-check mb-4">
<input type="checkbox" class="form-check-input" id="externalSourceCheck" [checked]="(contentSource?.harvestType !== harvestTypeNone)" (change)="changeExternalSource()">
<label class="form-check-label" for="externalSourceCheck">{{ 'collection.edit.tabs.source.external' | translate }}</label>
<input type="checkbox" class="form-check-input" id="externalSourceCheck"
[checked]="(contentSource?.harvestType !== harvestTypeNone)" (change)="changeExternalSource()">
<label class="form-check-label"
for="externalSourceCheck">{{ 'collection.edit.tabs.source.external' | translate }}</label>
</div>
<ds-loading *ngIf="!contentSource" [message]="'loading.content-source' | translate"></ds-loading>
<h4 *ngIf="contentSource && (contentSource?.harvestType !== harvestTypeNone)">{{ 'collection.edit.tabs.source.form.head' | translate }}</h4>
</div>
<div class="row">
<ds-form *ngIf="formGroup && contentSource && (contentSource?.harvestType !== harvestTypeNone)"
[formId]="'collection-source-form-id'"
[formGroup]="formGroup"
@@ -35,8 +39,11 @@
(dfChange)="onChange($event)"
(submitForm)="onSubmit()"
(cancel)="onCancel()"></ds-form>
<div class="container-fluid" *ngIf="(contentSource?.harvestType !== harvestTypeNone)">
<div class="d-inline-block float-right">
</div>
<div class="container mt-2" *ngIf="(contentSource?.harvestType !== harvestTypeNone)">
<div class="row">
<div class="col-12">
<div class="d-inline-block float-right ml-1">
<button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)"
(click)="discard()"><i
@@ -48,10 +55,20 @@
class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span>
</button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
<button class="btn btn-primary"
[disabled]="!(hasChanges() | async) || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
(click)="onSubmit()"><i
class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>
</button>
</div>
</div>
</div>
</div>
<ds-collection-source-controls
[isEnabled]="!(hasChanges()|async)"
[shouldShow]="contentSource?.harvestType !== harvestTypeNone"
[collection]="(collectionRD$ |async)?.payload"
>
</ds-collection-source-controls>

View File

@@ -380,7 +380,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
switchMap((uuid) => this.collectionService.getHarvesterEndpoint(uuid)),
take(1)
).subscribe((endpoint) => this.requestService.removeByHrefSubstring(endpoint));
this.requestService.setStaleByHrefSubstring(this.contentSource._links.self.href);
// Update harvester
this.collectionRD$.pipe(
getFirstSucceededRemoteData(),

View File

@@ -9,6 +9,7 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate
import { CollectionSourceComponent } from './collection-source/collection-source.component';
import { CollectionAuthorizationsComponent } from './collection-authorizations/collection-authorizations.component';
import { CollectionFormModule } from '../collection-form/collection-form.module';
import { CollectionSourceControlsComponent } from './collection-source/collection-source-controls/collection-source-controls.component';
/**
* Module that contains all components related to the Edit Collection page administrator functionality
@@ -26,6 +27,8 @@ import { CollectionFormModule } from '../collection-form/collection-form.module'
CollectionRolesComponent,
CollectionCurateComponent,
CollectionSourceComponent,
CollectionSourceControlsComponent,
CollectionAuthorizationsComponent
]
})

View File

@@ -138,7 +138,7 @@ export class CollectionDataService extends ComColDataService<Collection> {
* Get the collection's content harvester
* @param collectionId
*/
getContentSource(collectionId: string): Observable<RemoteData<ContentSource>> {
getContentSource(collectionId: string, useCachedVersionIfAvailable = true): Observable<RemoteData<ContentSource>> {
const href$ = this.getHarvesterEndpoint(collectionId).pipe(
isNotEmptyOperator(),
take(1)
@@ -146,7 +146,7 @@ export class CollectionDataService extends ComColDataService<Collection> {
href$.subscribe((href: string) => {
const request = new ContentSourceRequest(this.requestService.generateRequestId(), href);
this.requestService.send(request, true);
this.requestService.send(request, useCachedVersionIfAvailable);
});
return this.rdbService.buildSingle<ContentSource>(href$);

View File

@@ -70,6 +70,15 @@ export class ContentSource extends CacheableObject {
*/
metadataConfigs: MetadataConfig[];
@autoserializeAs('harvest_status')
harvestStatus: string;
@autoserializeAs('harvest_start_time')
harvestStartTime: string;
@autoserializeAs('last_harvested')
lastHarvested: string;
/**
* The {@link HALLink}s for this ContentSource
*/

View File

@@ -881,6 +881,22 @@
"collection.select.table.title": "Title",
"collection.source.controls.head": "Harvest Controls",
"collection.source.controls.test.submit.error": "Something went wrong with initiating the testing of the settings",
"collection.source.controls.test.failed": "The script to test the settings has failed",
"collection.source.controls.test.completed": "The script to test the settings has successfully finished",
"collection.source.controls.test.submit": "Test configuration",
"collection.source.controls.import.submit.success": "The import has been successfully initiated",
"collection.source.controls.import.submit.error": "Something went wrong with initiating the import",
"collection.source.controls.import.submit": "Import now",
"collection.source.controls.import.failed": "An error occurred during the import",
"collection.source.controls.import.completed": "The import completed",
"collection.source.controls.reset.submit": "Reset and reimport",
"collection.source.controls.harvest.status": "Harvest status:",
"collection.source.controls.harvest.start": "Harvest start time:",
"collection.source.controls.harvest.last": "Last time harvested:",
"collection.source.controls.harvest.no-information": "N/A",
"collection.source.update.notifications.error.content": "The provided settings have been tested and didn't work.",