From 35ff615bc6416aa7cd6fd63dac5e02dd456336fb Mon Sep 17 00:00:00 2001 From: Baptiste Toulemonde <toulemonde@cines.fr> Date: Fri, 23 Sep 2022 10:05:56 +0200 Subject: [PATCH] feature edit dataset --- src/app/app-routing.module.ts | 2 + src/app/app.module.ts | 2 + .../datasets/services/dataset-crud.service.ts | 2 +- src/app/edition/edition.component.html | 152 ++++++++++++++++ src/app/edition/edition.component.scss | 8 + src/app/edition/edition.component.spec.ts | 25 +++ src/app/edition/edition.component.ts | 168 ++++++++++++++++++ src/app/edition/service/edit.service.ts | 52 ++++++ src/app/mapping/class/dataset.ts | 1 + .../feedback-dialog.component.html | 1 + .../feedback-dialog.component.ts | 4 +- src/app/mapping/service/mapping.service.ts | 2 +- .../semantic-enrichment/ConceptsRequest.ts | 1 + src/app/stats/stats.component.html | 2 +- src/app/stats/stats.component.ts | 64 +++---- 15 files changed, 446 insertions(+), 40 deletions(-) create mode 100644 src/app/edition/edition.component.html create mode 100644 src/app/edition/edition.component.scss create mode 100644 src/app/edition/edition.component.spec.ts create mode 100644 src/app/edition/edition.component.ts create mode 100644 src/app/edition/service/edit.service.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 4100ee6b4..895fa509e 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -12,6 +12,7 @@ import { FdpGuard } from './authentication/services/fdp.guard'; import { RepositoryinfoComponent } from './repositoryinfo/repositoryinfo.component'; import { PublishApiComponent } from './publishapi/publishapi.component'; import {SemanticEnrichmentComponent} from './semantic-enrichment/semantic-enrichment.component'; +import {EditionComponent} from './edition/edition.component'; export interface ICustomRoute extends Route { name?: string; @@ -27,6 +28,7 @@ const routes: ICustomRoute[] = [ { path: 'repositoryinfo', component: RepositoryinfoComponent }, { path: 'stats', component: StatsComponent }, { path: 'publishapi', component: PublishApiComponent }, + { path: 'edit/:id', component: EditionComponent} ] }, {path: 'fdpsignin', component: SignupComponent, canActivate: [AuthGuardService]}, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 733dd5f71..f199402fe 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -48,6 +48,7 @@ import { SemanticEnrichmentComponent } from './semantic-enrichment/semantic-enri import {MatAutocompleteModule} from '@angular/material/autocomplete'; import {MatFormFieldModule} from '@angular/material/form-field'; import { OrderByScorePipe } from './semantic-enrichment/pipes/order-by-score.pipe'; +import { EditionComponent } from './edition/edition.component'; @@ -69,6 +70,7 @@ import { OrderByScorePipe } from './semantic-enrichment/pipes/order-by-score.pip CallbackComponent, SemanticEnrichmentComponent, OrderByScorePipe, + EditionComponent, ], imports: [ diff --git a/src/app/datasets/services/dataset-crud.service.ts b/src/app/datasets/services/dataset-crud.service.ts index b9cb55b1a..7a00f71dd 100644 --- a/src/app/datasets/services/dataset-crud.service.ts +++ b/src/app/datasets/services/dataset-crud.service.ts @@ -46,7 +46,7 @@ export class DatasetCrudService { httpOptions.append('Content-Type', 'application/rdf+xml'); httpOptions.append('Accept', 'application/rdf+xml'); httpOptions.append('Authorization', `Bearer ${this.smartHarvesterToken}`) - let url = `${SMARTHARVESTER_API}/harvester/api/transform?url=${xmlUrl}&catalogId=${catalogID}`; + const url = `${SMARTHARVESTER_API}/harvester/api/mapping?url=${xmlUrl}&catalogId=${catalogID}`; const myInit = { method: 'GET', headers: httpOptions, responseType: 'text' }; const myRequest = new Request(url, myInit); diff --git a/src/app/edition/edition.component.html b/src/app/edition/edition.component.html new file mode 100644 index 000000000..a648b16fa --- /dev/null +++ b/src/app/edition/edition.component.html @@ -0,0 +1,152 @@ +<ng-template #dialog let-data let-ref="dialogRef"> + <nb-card> + <nb-card-body>{{ messageError }}</nb-card-body> + <nb-card-footer> + <button nbButton (click)="getDatasetByCatalogId();ref.close()">Retry</button> + <button nbButton (click)="ref.close()">Close</button> + </nb-card-footer> + </nb-card> +</ng-template> + +<mat-autocomplete #auto="matAutocomplete" [displayWith]="viewHandle" (optionSelected)="setIri($event)"> + <mat-option + *ngFor="let option of filteredOptions | async" + [value]="option"> + {{option.source.document.label}} + </mat-option> +</mat-autocomplete> + +<ng-template #noConcept> + <nb-card> + <nb-card-body> + <h3>No concepts found...</h3> + </nb-card-body> + </nb-card> +</ng-template> + +<ng-template #noKeywordsFound> + <nb-card> + <nb-card-body> + <h3>No keywords found...</h3> + </nb-card-body> + </nb-card> +</ng-template> + +<ng-template #loading xmlns="http://www.w3.org/1999/html"> + <nb-card size="small" [nbSpinner]="true" nbSpinnerStatus="primary" nbSpinnerSize="giant"></nb-card> +</ng-template> +<ng-container *ngIf="!isLoading; else loading"> + <ng-container *ngFor="let keyword of datasets; let i = index"> + <nb-accordion> + <nb-accordion-item> + <nb-accordion-item-header><a href="{{keyword.url}}" target="_blank">{{keyword.title}}</a> + <div> + <nb-icon icon="checkmark-circle-2-outline" [status]=""></nb-icon> + </div> + </nb-accordion-item-header> + + <nb-accordion-item-body> + <nb-card *ngIf="keyword.keywords.length > 0; else noKeywordsFound" [size]="'tiny'"> + <nb-card-header> + Keyword(s) found + </nb-card-header> + <nb-list> + <nb-list-item *ngFor="let k of keyword.keywords"> + {{ k }} + </nb-list-item> + </nb-list> + </nb-card> + + <nb-card *ngIf="keyword.conceptIri.length > 0; else noConcept" [size]="'tiny'"> + <nb-card-header> + Concept iri found + </nb-card-header> + <nb-list> + <nb-list-item *ngFor="let k of keyword.conceptIri"> + {{ k }} + </nb-list-item> + </nb-list> + </nb-card> + <nb-card> + <nb-card-body> + <mat-form-field class="half-width"> + <input + type="text" + placeholder="enter a value with at least 4 characters" + [(ngModel)]="values[i]" + (ngModelChange)="onModelChange($event)" + matInput + [matAutocomplete]="auto" + (focusin)="setId(keyword.url)"> + + </mat-form-field> + <div class="mt0-m"> + <table *ngIf="autocompleteMap.get(keyword.url).length > 0"> + <thead style="background-color: #3366ff"> + <th></th> + <th class="text-center">label</th> + <th class="text-center">definition</th> + <th class="text-center">synonyms</th> + </thead> + <tbody> + <tr *ngFor="let value of autocompleteMap.get(keyword.url); let i = index "> + <td> + <button nbButton ghost> + <nb-icon icon="trash-2-outline" status="danger" + (click)="deleteProperty(keyword.url, i)"> + </nb-icon> + </button> + </td> + <td class="text-center"><a href="{{value.source.document.iri}}" + target="_blank">{{ value.source.document.label }}</a></td> + <td class="text-center">{{ value.source.document.description }}</td> + <td class="text-center">{{ value.source.document.synonyms }}</td> + + </tr> + </tbody> + </table> + </div> + </nb-card-body> + </nb-card> + <nb-card [size]="'small'" *ngIf="keyword.concepts"> + <table> + <thead style="background-color: #3366ff"> + <th></th> + <th class="text-center">label</th> + <th class="text-center">definition</th> + <th class="text-center">synonyms</th> + <th class="text-center">score</th> + </thead> + <tbody> + <tr *ngFor="let objet of keyword.concepts.results | orderByScore"> + <td class="text-center"> + <nb-checkbox [(checked)]="objet.checked"></nb-checkbox> + </td> + <td class="text-center"><a href="{{objet.source.document.iri}}" + target="_blank">{{ objet.source.document.label }}</a></td> + <td class="text-center">{{ objet.source.document.description }}</td> + <td class="text-center">{{ objet.source.document.synonyms }}</td> + <td class="text-center">{{ objet.score | number: '2.2-2' }}</td> + </tr> + </tbody> + </table> + </nb-card> + </nb-accordion-item-body> + </nb-accordion-item> + </nb-accordion> + + </ng-container> + + + <div class="row"> + <div class="button-center"> + <button nbButton status="primary" (click)="onSubmit()" [nbSpinner]="loadingPublish" [disabled]="loadingPublish" + nbSpinnerStatus="basic">Edit + </button> + </div> + </div> +</ng-container> + + + + diff --git a/src/app/edition/edition.component.scss b/src/app/edition/edition.component.scss new file mode 100644 index 000000000..98beafa84 --- /dev/null +++ b/src/app/edition/edition.component.scss @@ -0,0 +1,8 @@ +.button-center{ + vertical-align: middle; + margin: auto +} + +.half-width { + width: 50%; +} diff --git a/src/app/edition/edition.component.spec.ts b/src/app/edition/edition.component.spec.ts new file mode 100644 index 000000000..393a216f5 --- /dev/null +++ b/src/app/edition/edition.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditionComponent } from './edition.component'; + +describe('EditionComponent', () => { + let component: EditionComponent; + let fixture: ComponentFixture<EditionComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ EditionComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/edition/edition.component.ts b/src/app/edition/edition.component.ts new file mode 100644 index 000000000..75d94d5d8 --- /dev/null +++ b/src/app/edition/edition.component.ts @@ -0,0 +1,168 @@ +import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {EditService} from './service/edit.service'; +import {KeywordResponse} from '../mapping/class/dataset'; +import {Observable, of} from 'rxjs'; +import {Result} from '../semantic-enrichment/ESModel'; +import {map} from 'rxjs/operators'; +import {PostService} from '../semantic-enrichment/services/post.service'; +import {NbDialogService} from '@nebular/theme'; +import {MatAutocompleteSelectedEvent} from '@angular/material/autocomplete'; +import {ConceptsRequest, DataConcept} from '../semantic-enrichment/ConceptsRequest'; +import {FeedbackDialogComponent} from '../mapping/dialog/feedback-dialog/feedback-dialog.component'; +import {MatDialog} from '@angular/material/dialog'; + +@Component({ + selector: 'app-edition', + templateUrl: './edition.component.html', + styleUrls: ['./edition.component.scss'] +}) +export class EditionComponent implements OnInit { + + @ViewChild('dialog') dialog: TemplateRef<any>; + param: string; + datasets: KeywordResponse[] = []; + isLoading: boolean; + messageError: string; + filteredOptions: Observable<Result[]>; + autocompleteMap: Map<string, Result[]> = new Map<string, Result[]>(); + values: string[] = []; + id: string; + loadingPublish: boolean; + + + constructor(private route: ActivatedRoute, private service: EditService, + private semanticService: PostService, private dialogService: NbDialogService, + private matDialog: MatDialog) { + + } + + ngOnInit(): void { + this.getDatasetByCatalogId(); + + } + + getDatasetByCatalogId() { + this.isLoading = true; + this.route.params.subscribe(param => { + this.service.getCatalog(param.id).subscribe( + (response: KeywordResponse[]) => { + this.datasets = response, + this.datasets.forEach((resp: KeywordResponse) => { + this.autocompleteMap.set(resp.url, []); + if (resp.concepts) { + resp.concepts.results.forEach((result: Result) => { + result.checked = false; + }); + } + }); + }, + error => { + this.isLoading = false; + this.messageError = error.message; + this.openDialog(this.dialog); + }, + () => this.isLoading = false + ); + }); + } + filter(val: string): Observable<any> { + if (val.length > 3) { + return this.semanticService.getData(val) + .pipe( + map((response) => response.results )); + } + return of([]); + } + + openDialog(dialog: TemplateRef<any>) { + this.dialogService.open(dialog, { + context: { + }, + }); + } + + viewHandle(value: any) { + if (typeof value !== 'string' && typeof value !== 'undefined' && null !== value) { + if (value.source ) { + return value.source.document.label; + } + } + return value; + } + onModelChange(value: string) { + Promise.resolve(null).then(() => this.filteredOptions = this.filter(value)); + } + + setId(id: string) { + this.id = id; + } + + setIri(option: MatAutocompleteSelectedEvent) { + const iris = this.autocompleteMap.get(this.id); + if (iris) { + iris.push(option.option.value); + } + this.autocompleteMap.set(this.id, iris); + console.log(this.autocompleteMap); + } + + deleteProperty(datasetKeyword: string, i: number) { + const results: Result[] = this.autocompleteMap.get(datasetKeyword); + results.splice(i, 1); + this.autocompleteMap.set(datasetKeyword, results); + } + + + onSubmit() { + const mappingData = new ConceptsRequest(); + mappingData.fdpToken = this.service.fdpToken; + mappingData.dataConcepts = []; + this.datasets.forEach((data: KeywordResponse) => { + if ((null !== data.concepts && data.concepts.results.filter(e => e.checked).length > 0) || + this.autocompleteMap.get(data.url) && this.autocompleteMap.get(data.url).length > 0) { + const concepts = new DataConcept(); + concepts.url = data.url; + concepts.iris = []; + if (data.concepts) { + data.concepts.results.forEach((result: Result) => { + if (result.checked) { + concepts.iris.push(result.source.document.iri); + } + }); + } + if (this.autocompleteMap.get(data.url) && this.autocompleteMap.get(data.url).length > 0) { + this.autocompleteMap.get(data.url).forEach((result: Result) => concepts.iris.push(result.source.document.iri)); + } + mappingData.dataConcepts.push(concepts); + } + }); + + console.log(mappingData); + const postedDatasets = []; + const notPostedDatasets = []; + this.loadingPublish = true; + + this.service.editDataset(mappingData).subscribe((resp) => { + resp.publishedUrl.forEach(e => postedDatasets.push(e)); + resp.notPublishedUrl.forEach(e => notPostedDatasets.push(e)); + this.matDialog.open(FeedbackDialogComponent, { + data: { + postedMetadatas: postedDatasets, + notPostedMetadatas: notPostedDatasets + } + }).afterClosed().subscribe(); + }, + error => { + this.loadingPublish = false; + this.matDialog.open(FeedbackDialogComponent, { + data: { + errorMessage: error.message, + postedMetadatas: postedDatasets, + notPostedMetadatas: notPostedDatasets + } + }); + }, + () => this.loadingPublish = false); + } +} diff --git a/src/app/edition/service/edit.service.ts b/src/app/edition/service/edit.service.ts new file mode 100644 index 000000000..abd1f0ea8 --- /dev/null +++ b/src/app/edition/service/edit.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import {TokenStorageService} from '../../authentication/services/token-storage.service'; +import {environment} from '../../../environments/environment'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; +import {MetaModel} from '../../stats/MetaModel'; +import {Observable} from 'rxjs'; +import {KeywordResponse} from '../../mapping/class/dataset'; +import {ConceptsRequest} from '../../semantic-enrichment/ConceptsRequest'; + +@Injectable({ + providedIn: 'root' +}) +export class EditService { + + fdpToken = this.tokenService.getFDPToken(); + fdpURl = environment.fdpUrl; + smartharveserToken = this.tokenService.getToken(); + smartHarvesterUrl = environment.smartharvesterUrl; + + constructor(private tokenService: TokenStorageService, private http: HttpClient) { + } + + getCatalog(catId: string): Observable<KeywordResponse[]> { + const httpOptions = { + headers: new HttpHeaders({ + Authorization: 'Bearer ' + this.smartharveserToken + }) + }; + if (!environment.staging && !environment.production) { + this.fdpURl = this.fdpURl.replace(':8080', ''); + } + return this.http.get<KeywordResponse[]>( + `${this.smartHarvesterUrl}/harvester/api/catalog/${catId}?baseUrl=${this.fdpURl}&token=${this.fdpToken}`, + httpOptions + ); + } + + editDataset(data: ConceptsRequest): Observable<any> { + const httpOptionsFDP = { + headers: new HttpHeaders({ + Authorization: 'Bearer ' + this.smartharveserToken, + Accept: 'application/json', + ContentType: 'application/json' + }) + }; + if (!environment.staging && !environment.production) { + this.fdpURl = this.fdpURl.replace(':8080', ''); + } + + return this.http.put(`${this.smartHarvesterUrl}/harvester/api/mapping`, data, httpOptionsFDP); + } +} diff --git a/src/app/mapping/class/dataset.ts b/src/app/mapping/class/dataset.ts index 9d939c3bb..e35e74671 100644 --- a/src/app/mapping/class/dataset.ts +++ b/src/app/mapping/class/dataset.ts @@ -74,6 +74,7 @@ export class KeywordResponse { public url: string; public title: string; public keywords: string[]; + public conceptIri: string[]; public concepts: ESModel; } diff --git a/src/app/mapping/dialog/feedback-dialog/feedback-dialog.component.html b/src/app/mapping/dialog/feedback-dialog/feedback-dialog.component.html index be9d5cbb4..abd5b634c 100644 --- a/src/app/mapping/dialog/feedback-dialog/feedback-dialog.component.html +++ b/src/app/mapping/dialog/feedback-dialog/feedback-dialog.component.html @@ -3,6 +3,7 @@ <h3 style="text-align: center;">Feedback: </h3> </nb-card-header> <nb-card-body> + <p *ngIf="data.errorMessage">{{data.errorMessage}}</p> <nb-card *ngIf="data.notPostedMetadatas.length > 0"> <nb-card-body> <p class="error" >An error occurred while publishing the following Datasets: </p> diff --git a/src/app/mapping/dialog/feedback-dialog/feedback-dialog.component.ts b/src/app/mapping/dialog/feedback-dialog/feedback-dialog.component.ts index d6b6ecdea..fa3f7644f 100644 --- a/src/app/mapping/dialog/feedback-dialog/feedback-dialog.component.ts +++ b/src/app/mapping/dialog/feedback-dialog/feedback-dialog.component.ts @@ -9,10 +9,10 @@ import { NbDialogRef } from '@nebular/theme'; }) export class FeedbackDialogComponent implements OnInit { - + constructor(private dialog: MatDialogRef<FeedbackDialogComponent>, - @Inject(MAT_DIALOG_DATA) public data: {postedMetadatas: any[], notPostedMetadatas: any[]} ) { } + @Inject(MAT_DIALOG_DATA) public data: {errorMessage: string, postedMetadatas: any[], notPostedMetadatas: any[]} ) { } ngOnInit(): void { } diff --git a/src/app/mapping/service/mapping.service.ts b/src/app/mapping/service/mapping.service.ts index 32644e2c7..e3097fe84 100644 --- a/src/app/mapping/service/mapping.service.ts +++ b/src/app/mapping/service/mapping.service.ts @@ -160,7 +160,7 @@ export class MappingService { httpOptions.append('Accept', 'application/json'); httpOptions.append('Authorization', `Bearer ${this.fds2Token}`); const url = - `${this.smartHarvesterUrl}/harvester/api/transform/publish?fdpUrl=${this.fdpUrl}&catalogId=${catalogId}&isJsonpath=${isJsonpath}`; + `${this.smartHarvesterUrl}/harvester/api/mapping/publish?fdpUrl=${this.fdpUrl}&catalogId=${catalogId}&isJsonpath=${isJsonpath}`; const myInit = { method: 'POST', body: JSON.stringify(data), headers: httpOptions}; const myRequest = new Request(url, myInit); diff --git a/src/app/semantic-enrichment/ConceptsRequest.ts b/src/app/semantic-enrichment/ConceptsRequest.ts index ebb2ed93f..1e8d7c65a 100644 --- a/src/app/semantic-enrichment/ConceptsRequest.ts +++ b/src/app/semantic-enrichment/ConceptsRequest.ts @@ -9,4 +9,5 @@ export class ConceptsRequest { export class DataConcept { iris: string[]; id: string; + url: string; } diff --git a/src/app/stats/stats.component.html b/src/app/stats/stats.component.html index 5f7379a80..2777398cf 100644 --- a/src/app/stats/stats.component.html +++ b/src/app/stats/stats.component.html @@ -21,7 +21,7 @@ </thead> <tbody> <tr *ngFor="let cat of catalogList"> - <td style="text-align: center;">{{cat.title}}</td> + <td style="text-align: center;"><a [routerLink]="['/dashboard/edit', cat.catId]">{{cat.title}}</a></td> <td style="text-align: center;"><strong>{{cat.count}}</strong></td> </tr> </tbody> diff --git a/src/app/stats/stats.component.ts b/src/app/stats/stats.component.ts index c72b29ad2..7e1e85be2 100644 --- a/src/app/stats/stats.component.ts +++ b/src/app/stats/stats.component.ts @@ -23,37 +23,36 @@ export class StatsComponent implements OnInit { constructor(private parserService: ParseXmlService, private catalogService: CatalogService) { } ngOnInit(): void { + const query1 = 'prefix dct: <http://purl.org/dc/terms/> ' + + 'SELECT (COUNT(?s) AS ?triples) ' + + 'WHERE { ?s a <http://www.w3.org/ns/dcat#Catalog>; ' + + 'dct:isPartOf <' + environment.fdpUrl + '>}'; + this.parserService.getXmlResult(query1) + .subscribe(data => { + if (data) { + this.results = []; + data.results.bindings.forEach(element => { + this.results.push(element); + }); + this.stats.push(this.results[0]['triples'].value); - const query1 = 'prefix dct: <http://purl.org/dc/terms/> ' + - 'SELECT (COUNT(?s) AS ?triples) ' + - 'WHERE { ?s a <http://www.w3.org/ns/dcat#Catalog>; ' + - 'dct:isPartOf <' + environment.fdpUrl + '>}'; - this.parserService.getXmlResult(query1) - .subscribe(data => { - if (data) { - this.results = []; data.results.bindings.forEach(element => { - this.results.push(element); - }); - this.stats.push(this.results[0]["triples"].value); - - const query2 = 'prefix dct: <http://purl.org/dc/terms/> ' + - 'SELECT (COUNT(?s) AS ?triples) ' + - 'WHERE { ?s a <http://www.w3.org/ns/dcat#Dataset>;' + - ' dct:isPartOf* <' + environment.fdpUrl + '> }'; - this.parserService.getXmlResult(query2) - .subscribe(data => { - if (data) { - this.results = []; - data.results.bindings.forEach(element => { - this.results.push(element); - }); - this.stats.push(this.results[0]["triples"].value); } - }); - - console.log(this.stats); - } - }); - this.getCatalogIdByUser(); + const query2 = 'prefix dct: <http://purl.org/dc/terms/> ' + + 'SELECT (COUNT(?s) AS ?triples) ' + + 'WHERE { ?s a <http://www.w3.org/ns/dcat#Dataset>;' + + ' dct:isPartOf* <' + environment.fdpUrl + '> }'; + this.parserService.getXmlResult(query2) + .subscribe(data => { + if (data) { + this.results = []; + data.results.bindings.forEach(element => { + this.results.push(element); + }); + this.stats.push(this.results[0]['triples'].value); + } + }); + } + }); + this.getCatalogIdByUser(); } getCatalogIdByUser() { @@ -81,11 +80,6 @@ export class StatsComponent implements OnInit { ) ); }); - Promise.all(titlePromises).finally(() => { - if (this.catalogList.length > 0) { - this.catalogList.sort((a, b) => a.catId.localeCompare(b.catId)); - } - }); }, error: (error) => { if (error.staus === 404) { /* Ignore */ } -- GitLab