diff --git a/client/src/app/instance/search/components/result/datatable-actions.component.html b/client/src/app/instance/search/components/result/datatable-actions.component.html new file mode 100644 index 0000000000000000000000000000000000000000..a412b0e08dfd57bf0b2a2a6e65dfbf12e54492c8 --- /dev/null +++ b/client/src/app/instance/search/components/result/datatable-actions.component.html @@ -0,0 +1,32 @@ +<div class="btn-group mb-2" dropdown [isDisabled]="selectedData.length < 1"> + <button id="button-basic" dropdownToggle type="button" class="btn btn-primary dropdown-toggle" aria-controls="dropdown-basic"> + Actions <span class="caret"></span> + </button> + <ul id="dropdown-basic" *dropdownMenu class="dropdown-menu" role="menu" aria-labelledby="button-basic"> + <li *ngIf="getConfigDownloadResultFormat('download_csv')" role="menuitem"> + <a class="dropdown-item" [href]="getUrl('csv')" (click)="click($event, getUrl('csv'), 'csv')"> + <span class="fas fa-file-csv"></span> Download CSV + </a> + </li> + <li *ngIf="getConfigDownloadResultFormat('download_ascii')" role="menuitem"> + <a class="dropdown-item" [href]="getUrl('ascii')" (click)="click($event, getUrl('ascii'), 'txt')"> + <span class="fas fa-file"></span> Download ASCII + </a> + </li> + <li *ngIf="getConfigDownloadResultFormat('download_vo')" role="menuitem"> + <a class="dropdown-item" [href]="getUrl('votable')" (click)="click($event, getUrl('votable'), 'xml')"> + <span class="fas fa-file"></span> VOtable + </a> + </li> + <li *ngIf="getConfigDownloadResultFormat('download_vo')" role="menuitem"> + <a class="dropdown-item" (click)="broadcastVotable()"> + <span class="fas fa-broadcast-tower"></span> Broadcast VOtable + </a> + </li> + <li *ngIf="getConfigDownloadResultFormat('download_archive')" role="menuitem"> + <a class="dropdown-item" [href]="getUrlArchive()" (click)="click($event, getUrlArchive(), 'zip')"> + <span class="fas fa-archive"></span> Download files archive + </a> + </li> + </ul> +</div> diff --git a/client/src/app/instance/search/components/result/datatable-actions.component.ts b/client/src/app/instance/search/components/result/datatable-actions.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..fcd508e6f621b282e253704f93f56f049d2ea5c9 --- /dev/null +++ b/client/src/app/instance/search/components/result/datatable-actions.component.ts @@ -0,0 +1,107 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { Dataset, Attribute } from 'src/app/metamodel/models'; +import { Criterion, ConeSearch, criterionToString } from 'src/app/instance/store/models'; +import { getHost } from 'src/app/shared/utils'; +import { AppConfigService } from 'src/app/app-config.service'; + +@Component({ + selector: 'app-datatable-actions', + templateUrl: 'datatable-actions.component.html' +}) +export class DatatableActionsComponent { + @Input() selectedData: any[] = []; + @Input() datasetSelected: string; + @Input() datasetList: Dataset[]; + @Input() attributeList: Attribute[]; + @Input() criteriaList: Criterion[]; + @Input() outputList: number[]; + @Input() coneSearch: ConeSearch; + @Input() dataLength: number; + @Input() sampRegistered: boolean; + @Output() broadcast: EventEmitter<string> = new EventEmitter(); + + constructor(private appConfig: AppConfigService, private http: HttpClient) { } + + /** + * Checks if the download format is allowed by Anis Admin configuration. + * + * @param {string} format - The file format to download. + * + * @return boolean + */ + getConfigDownloadResultFormat(format: string): boolean { + const dataset = this.datasetList.find(d => d.name === this.datasetSelected); + return dataset.config.download[format]; + } + + /** + * Returns URL to download file for the given format. + * + * @param {string} format - The file format to download. + * + * @return string + */ + getUrl(format: string): string { + let query: string = `${getHost(this.appConfig.apiUrl)}/search/${this.datasetSelected}?a=${this.outputList.join(';')}`; + if (this.criteriaList.length > 0) { + query += `&c=${this.criteriaList.map(criterion => criterionToString(criterion)).join(';')};${this.getCriterionSelectedData()}`; + } else { + query += `&c=${this.getCriterionSelectedData()}`; + } + if (this.coneSearch) { + query += `&cs=${this.coneSearch.ra}:${this.coneSearch.dec}:${this.coneSearch.radius}`; + } + query += `&f=${format}`; + return query; + } + + getCriterionSelectedData() { + const attributeId = this.attributeList.find(a => a.search_flag === 'ID'); + return `${attributeId.id}::in::${this.selectedData.join('|')}`; + } + + /** + * Returns URL to download archive. + * + * @return boolean + */ + getUrlArchive(): string { + let query: string = `${getHost(this.appConfig.apiUrl)}/archive/${this.datasetSelected}?a=${this.outputList.join(';')}`; + if (this.criteriaList.length > 0) { + query += `&c=${this.criteriaList.map(criterion => criterionToString(criterion)).join(';')};${this.getCriterionSelectedData()}`; + } else { + query += `&c=${this.getCriterionSelectedData()}`; + } + if (this.coneSearch) { + query += `&cs=${this.coneSearch.ra}:${this.coneSearch.dec}:${this.coneSearch.radius}`; + } + return query; + } + + /** + * Emits event to action to broadcast data. + * + * @fires EventEmitter<string> + */ + broadcastVotable(): void { + this.broadcast.emit(this.getUrl('votable')); + } + + /** + * Allows to download file. + */ + click(event, href, extension): void { + event.preventDefault(); + + this.http.get(href, {responseType: "blob"}).subscribe( + data => { + let downloadLink = document.createElement('a'); + downloadLink.href = window.URL.createObjectURL(data); + downloadLink.setAttribute('download', `${this.datasetSelected}.${extension}`); + downloadLink.click(); + } + ); + } +} diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.html b/client/src/app/instance/search/components/result/datatable-tab.component.html index f9f6b3aaa6a57531613c217cf6a1ebf3501bc5be..b0ad6b0c98a437955dc1be5f759df0ce1f640a94 100644 --- a/client/src/app/instance/search/components/result/datatable-tab.component.html +++ b/client/src/app/instance/search/components/result/datatable-tab.component.html @@ -8,6 +8,18 @@ <span *ngIf="!ag.isOpen"><span class="fas fa-chevron-down"></span></span> </span> </button> + <app-datatable-actions + [selectedData]="selectedData" + [datasetSelected]="datasetSelected" + [datasetList]="datasetList" + [attributeList]="attributeList" + [criteriaList]="criteriaList" + [outputList]="outputList" + [coneSearch]="coneSearch" + [dataLength]="dataLength" + [sampRegistered]="sampRegistered" + (broadcast)="broadcast.emit($event)"> + </app-datatable-actions> <app-datatable [dataset]="datasetList | datasetByName:datasetSelected" [instance]="instance" diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.spec.ts b/client/src/app/instance/search/components/result/datatable-tab.component.spec.ts index 7705a42bee417a62090da3bbc6dceea580550981..3d261b62cf37b9689007c61b7de1a748793b5651 100644 --- a/client/src/app/instance/search/components/result/datatable-tab.component.spec.ts +++ b/client/src/app/instance/search/components/result/datatable-tab.component.spec.ts @@ -6,7 +6,7 @@ import { AccordionModule } from 'ngx-bootstrap/accordion'; import { DatatableTabComponent } from './datatable-tab.component'; import { Attribute, Dataset, Instance } from '../../../../metamodel/models'; -import { SearchQueryParams } from '../../../store/models'; +import { SearchQueryParams, Criterion, ConeSearch } from '../../../store/models'; import { DatasetByNamePipe } from '../../../../shared/pipes/dataset-by-name.pipe'; describe('[Instance][Search][Component][Result] DatatableTabComponent', () => { @@ -24,6 +24,19 @@ describe('[Instance][Search][Component][Result] DatatableTabComponent', () => { @Input() selectedData: any[] = []; } + @Component({ selector: 'app-datatable-actions', template: '' }) + class DatatableActionsStubComponent { + @Input() selectedData: any[] = []; + @Input() datasetSelected: string; + @Input() datasetList: Dataset[]; + @Input() attributeList: Attribute[]; + @Input() criteriaList: Criterion[]; + @Input() outputList: number[]; + @Input() coneSearch: ConeSearch; + @Input() dataLength: number; + @Input() sampRegistered: boolean; + } + let component: DatatableTabComponent; let fixture: ComponentFixture<DatatableTabComponent>; @@ -32,6 +45,7 @@ describe('[Instance][Search][Component][Result] DatatableTabComponent', () => { declarations: [ DatatableTabComponent, DatatableStubComponent, + DatatableActionsStubComponent, DatasetByNamePipe ], imports: [ diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.ts b/client/src/app/instance/search/components/result/datatable-tab.component.ts index ff703b86206de32c28b2a9660077808926cd2eec..c70564ba46caa5d342e2c80c5516221d549dab58 100644 --- a/client/src/app/instance/search/components/result/datatable-tab.component.ts +++ b/client/src/app/instance/search/components/result/datatable-tab.component.ts @@ -10,7 +10,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { Instance, Attribute, Dataset } from 'src/app/metamodel/models'; -import { Pagination, SearchQueryParams } from 'src/app/instance/store/models'; +import { Pagination, SearchQueryParams, Criterion, ConeSearch } from 'src/app/instance/store/models'; /** * @class @@ -27,8 +27,11 @@ export class DatatableTabComponent { @Input() datasetList: Dataset[]; @Input() attributeList: Attribute[]; @Input() outputList: number[]; + @Input() criteriaList: Criterion[]; + @Input() coneSearch: ConeSearch; @Input() queryParams: SearchQueryParams; @Input() dataLength: number; + @Input() sampRegistered: boolean; @Input() data: any[]; @Input() dataIsLoading: boolean; @Input() dataIsLoaded: boolean; @@ -36,4 +39,5 @@ export class DatatableTabComponent { @Output() retrieveData: EventEmitter<Pagination> = new EventEmitter(); @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter(); @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter(); + @Output() broadcast: EventEmitter<string> = new EventEmitter(); } diff --git a/client/src/app/instance/search/components/result/index.ts b/client/src/app/instance/search/components/result/index.ts index f8e4657e1f14cc050b279326e6434fd79d50a862..fffbcf4e766b53c4b4788371155f5e08e7b22207 100644 --- a/client/src/app/instance/search/components/result/index.ts +++ b/client/src/app/instance/search/components/result/index.ts @@ -4,6 +4,7 @@ import { ReminderComponent } from './reminder.component'; import { SampComponent } from './samp.component'; import { UrlDisplayComponent } from './url-display.component'; import {Â DatatableComponent } from './datatable.component'; +import { DatatableActionsComponent } from './datatable-actions.component'; import { rendererComponents } from './renderer'; export const resultComponents = [ @@ -13,5 +14,6 @@ export const resultComponents = [ SampComponent, UrlDisplayComponent, DatatableComponent, + DatatableActionsComponent, rendererComponents ]; diff --git a/client/src/app/instance/search/containers/result.component.html b/client/src/app/instance/search/containers/result.component.html index 93a2c7ac44fcdfbdb4dc13730bab79ae612d7c41..6ab41d2cd824b5b049c07165a48da4f90bdd5428 100644 --- a/client/src/app/instance/search/containers/result.component.html +++ b/client/src/app/instance/search/containers/result.component.html @@ -65,16 +65,19 @@ [instance]="instance | async" [datasetList]="datasetList | async" [attributeList]="attributeList | async | sortByOutputDisplay" + [criteriaList]="criteriaList | async" [outputList]="outputList | async" [queryParams]="queryParams | async" [dataLength]="dataLength | async" + [sampRegistered]="sampRegistered | async" [data]="data | async" [dataIsLoading]="dataIsLoading | async" [dataIsLoaded]="dataIsLoaded | async" [selectedData]="selectedData | async" (retrieveData)="retrieveData($event)" (addSelectedData)="addSearchData($event)" - (deleteSelectedData)="deleteSearchData($event)"> + (deleteSelectedData)="deleteSearchData($event)" + (broadcast)="broadcastVotable($event)"> </app-datatable-tab> </ng-container> </div> diff --git a/client/src/app/instance/search/containers/result.component.spec.ts b/client/src/app/instance/search/containers/result.component.spec.ts index 16462a1f25a8131e4d79b1dc6dd559b616e0b53f..37153740f4e191821a4e55836b3d93e155a7b1ee 100644 --- a/client/src/app/instance/search/containers/result.component.spec.ts +++ b/client/src/app/instance/search/containers/result.component.spec.ts @@ -66,8 +66,11 @@ describe('[Instance][Search][Container] ResultComponent', () => { @Input() datasetList: Dataset[]; @Input() attributeList: Attribute[]; @Input() outputList: number[]; + @Input() criteriaList: Criterion[]; + @Input() coneSearch: ConeSearch; @Input() queryParams: SearchQueryParams; @Input() dataLength: number; + @Input() sampRegistered: boolean; @Input() data: any[]; @Input() dataIsLoading: boolean; @Input() dataIsLoaded: boolean; diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index ef7580ab68e55a3d8f64d4b264ccb607a1bcfc79..1278c17faca8f6bd7e99e9424a136ad47f3890a3 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -51,6 +51,7 @@ import { sharedPipes } from './pipes'; CommonModule, FormsModule, ReactiveFormsModule, + BsDropdownModule, ModalModule, AccordionModule, PopoverModule,