diff --git a/client/package.json b/client/package.json index 4acaf2113c48ceee5372e722c4e9d34fc07d5c63..c085d335281027eb0572f85cac2166d3d66ae3e1 100644 --- a/client/package.json +++ b/client/package.json @@ -26,6 +26,7 @@ "@ngrx/store-devtools": "13.0.2", "bootstrap": "4.6.1", "d3": "^5.15.1", + "file-saver": "^2.0.5", "keycloak-angular": "^9.1.0", "keycloak-js": "^16.1.1", "ngx-bootstrap": "^8.0.0", @@ -40,6 +41,7 @@ "@angular/cli": "~13.2.3", "@angular/compiler-cli": "~13.2.2", "@types/d3": "^5.7.2", + "@types/file-saver": "^2.0.5", "@types/jasmine": "~3.10.0", "@types/jest": "^27.4.0", "@types/node": "^12.11.1", @@ -49,4 +51,4 @@ "jest-preset-angular": "^11.1.0", "typescript": "~4.5.5" } -} \ No newline at end of file +} diff --git a/client/src/app/instance/instance.reducer.ts b/client/src/app/instance/instance.reducer.ts index 3be0935a92e04797c5cd669a3721253f0bf81a81..be6b9a59f52e9af870a494c6ba7c55bf944263ac 100644 --- a/client/src/app/instance/instance.reducer.ts +++ b/client/src/app/instance/instance.reducer.ts @@ -15,7 +15,7 @@ import * as searchMultiple from './store/reducers/search-multiple.reducer'; import * as coneSearch from './store/reducers/cone-search.reducer'; import * as detail from './store/reducers/detail.reducer'; import * as svomJsonKw from './store/reducers/svom-json-kw.reducer'; -import * as downloadFile from './store/reducers/download-file.reducer'; +import * as archive from './store/reducers/archive.reducer'; /** * Interface for instance state. @@ -28,7 +28,7 @@ export interface State { coneSearch: coneSearch.State detail: detail.State, svomJsonKw: svomJsonKw.State, - downloadFile: downloadFile.State + archive: archive.State } const reducers = { @@ -37,7 +37,7 @@ const reducers = { coneSearch: coneSearch.coneSearchReducer, detail: detail.detailReducer, svomJsonKw: svomJsonKw.svomJsonKwReducer, - downloadFile: downloadFile.fileReducer + archive: archive.archiveReducer }; export const instanceReducer = combineReducers(reducers); diff --git a/client/src/app/instance/search/components/result/abstract-download.component.ts b/client/src/app/instance/search/components/result/abstract-download.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..16e1f4dd6b397660c9319b6ae603f88e3f46af37 --- /dev/null +++ b/client/src/app/instance/search/components/result/abstract-download.component.ts @@ -0,0 +1,80 @@ +import { Directive, Input, Output, EventEmitter } from '@angular/core'; + +import { Dataset } from 'src/app/metamodel/models'; +import { Criterion, ConeSearch, criterionToString } from 'src/app/instance/store/models'; +import { AppConfigService } from 'src/app/app-config.service'; +import { getHost } from 'src/app/shared/utils'; + +@Directive() +export abstract class AbstractDownloadComponent { + @Input() dataset: Dataset; + @Input() criteriaList: Criterion[]; + @Input() outputList: number[]; + @Input() coneSearch: ConeSearch; + @Input() archiveIsCreating: boolean; + @Output() downloadFile: EventEmitter<{url: string, filename: string}> = new EventEmitter(); + + constructor(private appConfig: AppConfigService) { } + + /** + * Returns API URL to get data with user parameters. + * + * @return string + */ + getUrl(format: string, selectedData: string = null): string { + return `${getHost(this.appConfig.apiUrl)}/search/${this.getQuery(selectedData)}&f=${format}`; + } + + getQuery(selectedData: string = null) { + let query = `${this.dataset.name}?a=${this.outputList.join(';')}`; + if (this.criteriaList.length > 0) { + query += `&c=${this.criteriaList.map(criterion => criterionToString(criterion)).join(';')}`; + if (selectedData) { + query += `;${selectedData}`; + } + } else if (selectedData) { + query += `&c=${selectedData}`; + } + if (this.coneSearch) { + query += `&cs=${this.coneSearch.ra}:${this.coneSearch.dec}:${this.coneSearch.radius}`; + } + return query; + } + + download(event, url: string, format: string) { + event.preventDefault(); + + const timeElapsed = Date.now(); + const today = new Date(timeElapsed); + const filename = `result_${this.dataset.name}_${today.toISOString()}.${this.formatToExtension(format)}`; + + this.downloadFile.emit({ url, filename }); + } + + formatToExtension(format: string) { + let extension: string; + switch (format) { + case 'json': { + extension = 'json'; + break; + } + case 'csv': { + extension = 'csv'; + break; + } + case 'ascii': { + extension = 'txt'; + break; + } + case 'votable': { + extension = 'xml'; + break; + } + default: { + extension = 'json'; + break; + } + } + return extension; + } +} \ No newline at end of file diff --git a/client/src/app/instance/search/components/result/cone-search-plot.component.html b/client/src/app/instance/search/components/result/cone-search-plot.component.html index 75192165db8f2c2ca8b91d8e614319569acdbdea..0d97ee1d4ae5dd8fbddf3e4d3334a991d6df0993 100644 --- a/client/src/app/instance/search/components/result/cone-search-plot.component.html +++ b/client/src/app/instance/search/components/result/cone-search-plot.component.html @@ -1 +1 @@ -<div id="plot" class="row bg-light"></div> \ No newline at end of file +<div id="plot" class="row justify-content-center"></div> \ No newline at end of file diff --git a/client/src/app/instance/search/components/result/cone-search-plot.component.ts b/client/src/app/instance/search/components/result/cone-search-plot.component.ts index e57aa634c5bca4bb4d55476c965320d837ca57c7..f2990528eb1e8ab673336c9ee704c66aa1d9cbb1 100644 --- a/client/src/app/instance/search/components/result/cone-search-plot.component.ts +++ b/client/src/app/instance/search/components/result/cone-search-plot.component.ts @@ -7,13 +7,12 @@ * file that was distributed with this source code. */ -import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation, OnInit, SimpleChanges } from '@angular/core'; +import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation, OnInit } from '@angular/core'; import * as d3 from 'd3'; -import { Dataset, Image } from 'src/app/metamodel/models'; +import { Dataset } from 'src/app/metamodel/models'; import { ConeSearch } from 'src/app/instance/store/models'; -import { AppConfigService } from 'src/app/app-config.service'; /** * @class @@ -30,7 +29,7 @@ export class ConeSearchPlotComponent implements OnInit { @Input() coneSearch: ConeSearch; @Input() dataset: Dataset; @Input() data: {x: number, y: number}[]; - @Input() selectedBackground: Image; + @Input() backgroundHref: string; // Interactive variables intialisation margin = { top: 50, right: 50, bottom: 50 , left: 50 }; @@ -41,42 +40,10 @@ export class ConeSearchPlotComponent implements OnInit { x: d3.ScaleLinear<number, number>; y: d3.ScaleLinear<number, number>; - constructor(private config: AppConfigService) { } - ngOnInit(): void { this.coneSearchPlot(); } - ngOnChanges(changes: SimpleChanges) { - if (changes.selectedBackground && changes.selectedBackground.currentValue) { - console.log('coucou'); - this.image.attr('xlink:href', this.getHrefBackgroundImage()); - } - } - - - getHrefBackgroundImage() { - if (this.selectedBackground) { - let href = `${this.config.servicesUrl}/fits-cut-to-png/${this.dataset.name}?filename=${this.selectedBackground.file_path}`; - href += `&ra=${this.coneSearch.ra}`; - href += `&dec=${this.coneSearch.dec}`; - href += `&radius=${this.coneSearch.radius}`; - href += `&stretch=${this.selectedBackground.stretch}`; - href += `&pmin=${this.selectedBackground.pmin}`; - href += `&pmax=${this.selectedBackground.pmax}`; - href += `&axes=false`; - return href; - } else { - const scale = this.coneSearch.radius / this.width; // arcsec/pix - return `https://skyserver.sdss.org/dr16/SkyServerWS/ImgCutout/getjpeg?TaskName=Skyserver.Chart.Image - &ra=${this.coneSearch.ra} - &dec=${this.coneSearch.dec} - &scale=${scale} - &width=${this.width} - &height=${this.height}`; - } - } - coneSearchPlot(): void { // Init SVG const svg = d3.select('#plot').append('svg') @@ -93,7 +60,7 @@ export class ConeSearchPlotComponent implements OnInit { // Background image this.image = svg.append('image'); - this.image.attr('xlink:href', this.getHrefBackgroundImage()) + this.image.attr('xlink:href', this.backgroundHref) .attr('width', this.width) .attr('height', this.height); 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 index d20f9c16448339958b173ae5a7b146b50728a98c..3889169ed51cc03145b4e17198f16706b4227be2 100644 --- a/client/src/app/instance/search/components/result/datatable-actions.component.html +++ b/client/src/app/instance/search/components/result/datatable-actions.component.html @@ -1,30 +1,35 @@ -<div *ngIf="getDataset().datatable_selectable_rows" class="btn-group mb-2" dropdown [isDisabled]="selectedData.length < 1"> +<div *ngIf="dataset.datatable_selectable_rows" 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" (click)="downloadResult('csv')"> + <li *ngIf="dataset.download_json" role="menuitem"> + <a class="dropdown-item" [href]="getDatatableUrl('json')" (click)="download($event, getDatatableUrl('json'), 'json')"> + <span class="fas fa-file"></span> Download JSON + </a> + </li> + <li *ngIf="dataset.download_csv" role="menuitem"> + <a class="dropdown-item" [href]="getDatatableUrl('csv')" (click)="download($event, getDatatableUrl('csv'), 'csv')"> <span class="fas fa-file-csv"></span> Download CSV </a> </li> - <li *ngIf="getConfigDownloadResultFormat('download_ascii')" role="menuitem"> - <a class="dropdown-item" (click)="downloadResult('ascii')"> + <li *ngIf="dataset.download_ascii" role="menuitem"> + <a class="dropdown-item" [href]="getDatatableUrl('ascii')" (click)="download($event, getDatatableUrl('ascii'), 'ascii')"> <span class="fas fa-file"></span> Download ASCII </a> </li> - <li *ngIf="getConfigDownloadResultFormat('download_vo')" role="menuitem"> - <a class="dropdown-item" (click)="downloadResult('votable')"> + <li *ngIf="dataset.download_vo" role="menuitem"> + <a class="dropdown-item" [href]="getDatatableUrl('votable')" (click)="download($event, getDatatableUrl('votable'), 'votable')"> <span class="fas fa-file"></span> VOtable </a> </li> - <li *ngIf="getConfigDownloadResultFormat('download_vo')" role="menuitem" [class.disabled]="!sampRegistered"> + <li *ngIf="dataset.download_vo" role="menuitem" [class.disabled]="!sampRegistered"> <a class="dropdown-item" [class.disabled]="!sampRegistered" (click)="broadcastResult()"> <span class="fas fa-broadcast-tower"></span> Broadcast VOtable </a> </li> - <li *ngIf="getConfigDownloadResultFormat('download_archive')" role="menuitem"> - <a class="dropdown-item" (click)="downloadArchive()"> + <li *ngIf="isArchiveIsAvailable()" role="menuitem" [class.disabled]="archiveIsCreating"> + <a class="dropdown-item" [class.disabled]="archiveIsCreating" (click)="downloadArchive()"> <span class="fas fa-archive"></span> Download files archive </a> </li> 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 index ac49911e931c23f40ee960e412eb20e8cde68e43..5d13989cfebdefa05ac1a4a9d99abee5842ec21a 100644 --- a/client/src/app/instance/search/components/result/datatable-actions.component.ts +++ b/client/src/app/instance/search/components/result/datatable-actions.component.ts @@ -1,53 +1,38 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; -import { Dataset } from 'src/app/metamodel/models'; +import { AbstractDownloadComponent } from './abstract-download.component'; +import { Attribute } from 'src/app/metamodel/models'; @Component({ selector: 'app-datatable-actions', templateUrl: 'datatable-actions.component.html' }) -export class DatatableActionsComponent { +export class DatatableActionsComponent extends AbstractDownloadComponent { + @Input() attributeList: Attribute[]; @Input() selectedData: any[] = []; - @Input() datasetSelected: string; - @Input() datasetList: Dataset[]; @Input() sampRegistered: boolean; - @Output() startTaskCreateResult: EventEmitter<{ format: string, selectedData: boolean, broadcastVo: boolean }> = new EventEmitter(); - @Output() startTaskCreateArchive: EventEmitter<{ selectedData: boolean }> = new EventEmitter(); + @Output() broadcastVotable: EventEmitter<string> = new EventEmitter(); + @Output() startTaskCreateArchive: EventEmitter<string> = new EventEmitter(); - /** - * 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 { - return this.getDataset()[format]; + isArchiveIsAvailable() { + return this.attributeList + .filter(attribute => this.outputList.includes(attribute.id)) + .filter(attribute => attribute.archive) + .length > 0; } - getDataset() { - return this.datasetList.find(d => d.name === this.datasetSelected); + getDatatableUrl(format: string): string { + const attributeId = this.attributeList.find(a => a.primary_key); + return this.getUrl(format, `${attributeId.id}::in::${this.selectedData.join('|')}`); } - - downloadResult(format: string) { - this.startTaskCreateResult.emit({ - format, - selectedData: true, - broadcastVo: false - }); - } - + broadcastResult() { - this.startTaskCreateResult.emit({ - format: 'votable', - selectedData: true, - broadcastVo: true - }) + const url = this.getDatatableUrl('votable'); + this.broadcastVotable.emit(url); } downloadArchive() { - this.startTaskCreateArchive.emit({ - selectedData: true - }); + const attributeId = this.attributeList.find(a => a.primary_key); + this.startTaskCreateArchive.emit(this.getQuery(`${attributeId.id}::in::${this.selectedData.join('|')}`)); } } 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 deleted file mode 100644 index d35da3bb90fac0e8f942488940b1827a7135561c..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/result/datatable-tab.component.html +++ /dev/null @@ -1,59 +0,0 @@ -<accordion *ngIf="(datasetList | datasetByName:datasetSelected).datatable_enabled" [isAnimated]="true"> - <accordion-group #ag [isOpen]="(datasetList | datasetByName:datasetSelected).datatable_opened" [panelClass]="'custom-accordion'" class="my-2"> - <button class="btn btn-link btn-block clearfix" accordion-heading> - <span class="pull-left float-left"> - Display result details - - <span *ngIf="ag.isOpen"><span class="fas fa-chevron-up"></span></span> - <span *ngIf="!ag.isOpen"><span class="fas fa-chevron-down"></span></span> - </span> - </button> - <div class="row"> - <div class="col-md-5" *ngIf="coneSearch"> - <div class="form-group"> - <label for="file_size">Background image</label> - <ng-select [(ngModel)]="selectedBackground"> - <ng-option *ngFor="let image of imageList" [value]="image">{{ image.file_path }}</ng-option> - </ng-select> - </div> - <app-cone-search-plot *ngIf="dataIsLoaded" - [coneSearch]="coneSearch" - [dataset]="datasetList | datasetByName:datasetSelected" - [data]="getData()" - [selectedBackground]="selectedBackground"> - </app-cone-search-plot> - </div> - <div class="datatable-group" [ngClass]="{'col': !coneSearch, 'col-md-7' : coneSearch }"> - <div class="row"> - <div class="col"> - <app-datatable-actions - [selectedData]="selectedData" - [datasetSelected]="datasetSelected" - [datasetList]="datasetList" - [sampRegistered]="sampRegistered" - (broadcast)="broadcast.emit($event)" - (startTaskCreateResult)="startTaskCreateResult.emit($event)" - (startTaskCreateArchive)="startTaskCreateArchive.emit($event)"> - </app-datatable-actions> - <app-datatable - [dataset]="datasetList | datasetByName:datasetSelected" - [instance]="instance" - [attributeList]="attributeList" - [outputList]="outputList" - [queryParams]="queryParams" - [dataLength]="dataLength" - [data]="data" - [dataIsLoading]="dataIsLoading" - [dataIsLoaded]="dataIsLoaded" - [selectedData]="selectedData" - (retrieveData)="retrieveData.emit($event)" - (addSelectedData)="addSelectedData.emit($event)" - (deleteSelectedData)="deleteSelectedData.emit($event)" - (downloadFile)="downloadFile.emit($event)"> - </app-datatable> - </div> - </div> - </div> - </div> - </accordion-group> -</accordion> diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.scss b/client/src/app/instance/search/components/result/datatable-tab.component.scss deleted file mode 100644 index 96286643764a50077a9f90b8a8776616bb1c80af..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/result/datatable-tab.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -.datatable-group > .row { - overflow-x: auto; - white-space: nowrap; -} -.datatable-group > .row > .col { - display: inline-block; - float: none; -} \ No newline at end of file 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 deleted file mode 100644 index 3d261b62cf37b9689007c61b7de1a748793b5651..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/result/datatable-tab.component.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { AccordionModule } from 'ngx-bootstrap/accordion'; - -import { DatatableTabComponent } from './datatable-tab.component'; -import { Attribute, Dataset, Instance } from '../../../../metamodel/models'; -import { SearchQueryParams, Criterion, ConeSearch } from '../../../store/models'; -import { DatasetByNamePipe } from '../../../../shared/pipes/dataset-by-name.pipe'; - -describe('[Instance][Search][Component][Result] DatatableTabComponent', () => { - @Component({ selector: 'app-datatable', template: '' }) - class DatatableStubComponent { - @Input() dataset: Dataset; - @Input() instance: Instance; - @Input() attributeList: Attribute[]; - @Input() outputList: number[]; - @Input() queryParams: SearchQueryParams; - @Input() dataLength: number; - @Input() data: any[]; - @Input() dataIsLoading: boolean; - @Input() dataIsLoaded: boolean; - @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>; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - DatatableTabComponent, - DatatableStubComponent, - DatatableActionsStubComponent, - DatasetByNamePipe - ], - imports: [ - AccordionModule.forRoot(), - BrowserAnimationsModule - ] - }); - fixture = TestBed.createComponent(DatatableTabComponent); - component = fixture.componentInstance; - }); - - it('should create the component', () => { - expect(component).toBeTruthy(); - }); -}); 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 deleted file mode 100644 index 239dde5745afd6c0239ff2185b6686ce28c5abd1..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/result/datatable-tab.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; - -import { Instance, Attribute, Dataset, Image } from 'src/app/metamodel/models'; -import { Pagination, SearchQueryParams, Criterion, ConeSearch } from 'src/app/instance/store/models'; - -/** - * @class - * @classdesc Search result datatable tab component. - */ -@Component({ - selector: 'app-datatable-tab', - templateUrl: 'datatable-tab.component.html', - styleUrls: [ 'datatable-tab.component.scss' ], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DatatableTabComponent { - @Input() datasetSelected: string; - @Input() instance: Instance; - @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; - @Input() selectedData: any[]; - @Input() imageList: Image[]; - @Input() imageListIsLoading: boolean; - @Input() imageListIsLoaded: boolean; - @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(); - @Output() startTaskCreateResult: EventEmitter<{ format: string, selectedData: boolean, broadcastVo: boolean }> = new EventEmitter(); - @Output() startTaskCreateArchive: EventEmitter<{ selectedData: boolean }> = new EventEmitter(); - @Output() downloadFile: EventEmitter<{url: string, fileId: string, datasetName: string, filename: string}> = new EventEmitter(); - - selectedBackground: Image = null; - - getData() { - const dataset = this.getDataset(); - const columnRa = this.attributeList.find(a => a.id === dataset.cone_search_column_ra); - const columnDec = this.attributeList.find(a => a.id === dataset.cone_search_column_dec); - return this.data.map(d => ({ "x": +d[columnRa.label], "y": +d[columnDec.label] })); - } - - /** - * Returns selected dataset for the search. - * - * @return Dataset - */ - getDataset(): Dataset { - return this.datasetList.find(dataset => dataset.name === this.datasetSelected); - } -} diff --git a/client/src/app/instance/search/components/result/datatable.component.scss b/client/src/app/instance/search/components/result/datatable.component.scss index 4d4085831e280f22c13f1d70d5e164691ffca869..d40e1d1dac814cf5d102188e107a1358420f18ff 100644 --- a/client/src/app/instance/search/components/result/datatable.component.scss +++ b/client/src/app/instance/search/components/result/datatable.component.scss @@ -7,11 +7,6 @@ * file that was distributed with this source code. */ -.table-responsive { - overflow-y: scroll; - height: 650px; -} - table th:not(.select) { min-width: 130px; } diff --git a/client/src/app/instance/search/components/result/datatable.component.ts b/client/src/app/instance/search/components/result/datatable.component.ts index 739b27df65417b20b3d92c1db34b346375ca719b..95cd763f07618d853931201f0268fa447ae117d9 100644 --- a/client/src/app/instance/search/components/result/datatable.component.ts +++ b/client/src/app/instance/search/components/result/datatable.component.ts @@ -44,7 +44,7 @@ export class DatatableComponent implements OnInit { @Output() retrieveData: EventEmitter<Pagination> = new EventEmitter(); @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter(); @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter(); - @Output() downloadFile: EventEmitter<{url: string, fileId: string, datasetName: string, filename: string}> = new EventEmitter(); + @Output() downloadFile: EventEmitter<{url: string, filename: string}> = new EventEmitter(); public page = 1; public nbItems = 10; diff --git a/client/src/app/instance/search/components/result/download-file-tab.component.html b/client/src/app/instance/search/components/result/download-file-tab.component.html deleted file mode 100644 index c4032c0b8fc06a3f80ac8c5b7cbeecf79e22bf57..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/result/download-file-tab.component.html +++ /dev/null @@ -1,16 +0,0 @@ -<div class="jumbotron mb-4 py-4"> - <div class="lead"> - Files downloaded : - <ul> - <li *ngFor="let downloadFile of downloadedFiles"> - {{ downloadFile.fileName }} : - <ng-container *ngIf="downloadFile.state == 'PENDING'"> - <br> - <span class="fas fa-circle-notch fa-spin"></span> - <span class="sr-only">Loading...</span> - </ng-container> - <progressbar [value]="downloadFile.progress" [type]="getType(downloadFile.progress)" [animate]="true">{{ downloadFile.progress }}%</progressbar> - </li> - </ul> - </div> -</div> \ No newline at end of file diff --git a/client/src/app/instance/search/components/result/download-file-tab.component.scss b/client/src/app/instance/search/components/result/download-file-tab.component.scss deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/client/src/app/instance/search/components/result/download-file-tab.component.ts b/client/src/app/instance/search/components/result/download-file-tab.component.ts deleted file mode 100644 index 95d1e5b6389f9ef6b91263070574821c752901d9..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/result/download-file-tab.component.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input } from '@angular/core'; - -import { DownloadFile } from 'src/app/instance/store/models'; - -/** - * @class - * @classdesc Search result reminder component. - */ -@Component({ - selector: 'app-download-file-tab', - templateUrl: 'download-file-tab.component.html', - styleUrls: ['download-file-tab.component.scss'] -}) -export class DownloadFileTabComponent { - @Input() downloadedFiles: DownloadFile[]; - - getType(value: number): 'success' | 'info' { - if (value < 100) { - return 'info'; - } - - return 'success'; - } -} diff --git a/client/src/app/instance/search/components/result/download-result.component.html b/client/src/app/instance/search/components/result/download-result.component.html new file mode 100644 index 0000000000000000000000000000000000000000..f85406687cd5304dddea4e92d8ab04edde4ef045 --- /dev/null +++ b/client/src/app/instance/search/components/result/download-result.component.html @@ -0,0 +1,47 @@ +<table> + <tr> + <th>Download results </th> + <td> + <a *ngIf="dataset.download_json" [href]="getUrl('json')" (click)="download($event, getUrl('json'), 'json')" class="btn btn-primary" title="Download results in JSON format"> + <span class="fas fa-file"></span> JSON + </a> + + <a *ngIf="dataset.download_csv" [href]="getUrl('csv')" (click)="download($event, getUrl('csv'), 'csv')" class="btn btn-primary" title="Download results in CSV format"> + <span class="fas fa-file-csv"></span> CSV + </a> + + <a *ngIf="dataset.download_ascii" [href]="getUrl('ascii')" (click)="download($event, getUrl('ascii'), 'ascii')" class="btn btn-primary" title="Download results in ASCII format"> + <span class="fas fa-file"></span> ASCII + </a> + + <a *ngIf="dataset.download_vo" [href]="getUrl('votable')" (click)="download($event, getUrl('votable'), 'votable')" class="btn btn-primary" title="Download results in VO format"> + <span class="fas fa-file"></span> VOtable + </a> + </td> + </tr> + <tr *ngIf="isArchiveIsAvailable()"> + <th>Download files </th> + <td> + <a [class.disabled]="archiveIsCreating" (click)="downloadArchive()" class="btn btn-primary" title="Download an archive with all files"> + <span class="fas fa-archive"></span> Files archive + </a> + </td> + </tr> + <tr *ngIf="instance.samp_enabled"> + <th>VO-SAMP </th> + <td> + <button *ngIf="!sampRegistered" (click)="sampRegister.emit()" class="btn btn-primary">Try to register</button> + <button *ngIf="sampRegistered" (click)="sampUnregister.emit()" class="btn btn-danger">SAMP Unregister</button> + + <button *ngIf="sampRegistered && dataset.download_vo" (click)="broadcastResult()" class="btn btn-primary" title="Broadcast samp votable"> + <span class="fas fa-broadcast-tower"></span> Broadcast VOtable + </button> + </td> + </tr> +</table> + +<p *ngIf="archiveIsCreating" class="text-center mt-2 text-danger font-weight-bold"> + Archive is under construction ! Please stay on this page + <span class="fas fa-circle-notch fa-spin fa-2x"></span> + <span class="sr-only">Loading...</span> +</p> \ No newline at end of file diff --git a/client/src/app/instance/search/components/result/download-result.component.scss b/client/src/app/instance/search/components/result/download-result.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..f27c2de02bf06a9cc68a93abf3b16a80ab25bb0e --- /dev/null +++ b/client/src/app/instance/search/components/result/download-result.component.scss @@ -0,0 +1,8 @@ +th { + font-weight: normal; + width: 200px; +} + +tr { + height: 50px; +} \ No newline at end of file diff --git a/client/src/app/instance/search/components/result/download-result.component.ts b/client/src/app/instance/search/components/result/download-result.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a25094ca2fba3e58afa7b95eb8d01a98481a874 --- /dev/null +++ b/client/src/app/instance/search/components/result/download-result.component.ts @@ -0,0 +1,35 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; + +import { AbstractDownloadComponent } from './abstract-download.component'; +import { Instance, Attribute } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-download-result', + templateUrl: 'download-result.component.html', + styleUrls: ['download-result.component.scss'] +}) +export class DownloadResultComponent extends AbstractDownloadComponent{ + @Input() instance: Instance; + @Input() attributeList: Attribute[]; + @Input() sampRegistered: boolean; + @Output() sampRegister: EventEmitter<{}> = new EventEmitter(); + @Output() sampUnregister: EventEmitter<{}> = new EventEmitter(); + @Output() broadcastVotable: EventEmitter<string> = new EventEmitter(); + @Output() startTaskCreateArchive: EventEmitter<string> = new EventEmitter(); + + isArchiveIsAvailable() { + return this.attributeList + .filter(attribute => this.outputList.includes(attribute.id)) + .filter(attribute => attribute.archive) + .length > 0; + } + + broadcastResult() { + const url = this.getUrl('votable'); + this.broadcastVotable.emit(url); + } + + downloadArchive() { + this.startTaskCreateArchive.emit(this.getQuery()); + } +} \ No newline at end of file diff --git a/client/src/app/instance/search/components/result/download.component.html b/client/src/app/instance/search/components/result/download.component.html deleted file mode 100644 index d4fa1f18feaeef270109a562c23753688759e7de..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/result/download.component.html +++ /dev/null @@ -1,55 +0,0 @@ -<accordion *ngIf="isDownloadActivated()" [isAnimated]="true"> - <accordion-group #ag [isOpen]="isDownloadOpened()" [panelClass]="'custom-accordion'" class="my-2"> - <button class="btn btn-link btn-block clearfix" accordion-heading> - <span class="pull-left float-left"> - Download results - - <span *ngIf="ag.isOpen"> - <span class="fas fa-chevron-up"></span> - </span> - <span *ngIf="!ag.isOpen"> - <span class="fas fa-chevron-down"></span> - </span> - </span> - </button> - <div> - <div class="row"> - <div class="col-auto align-self-center"> - <p>Download results just here:</p> - </div> - <div class="col"> - <a *ngIf="getConfigDownloadResultFormat('download_json')" (click)="downloadResult('json')" class="btn btn-outline-primary" title="Download results in JSON format"> - <span class="fas fa-file"></span> JSON - </a> - - <a *ngIf="getConfigDownloadResultFormat('download_csv')" (click)="downloadResult('csv')" class="btn btn-outline-primary" title="Download results in CSV format"> - <span class="fas fa-file-csv"></span> CSV - </a> - - <a *ngIf="getConfigDownloadResultFormat('download_ascii')" (click)="downloadResult('ascii')" class="btn btn-outline-primary" title="Download results in ASCII format"> - <span class="fas fa-file"></span> ASCII - </a> - - <a *ngIf="getConfigDownloadResultFormat('download_vo')" (click)="downloadResult('votable')" class="btn btn-outline-primary" title="Download results in VO format"> - <span class="fas fa-file"></span> VOtable - </a> - - <button *ngIf="getConfigDownloadResultFormat('download_vo')" [disabled]="!sampRegistered" (click)="broadcastResult()" class="btn btn-outline-primary" title="Broadcast samp votable"> - <span class="fas fa-broadcast-tower"></span> Broadcast VOtable - </button> - </div> - </div> - <hr *ngIf="isArchiveIsAvailable()" class="my-4"> - <div *ngIf="isArchiveIsAvailable()" class="row"> - <div class="col-auto align-self-center"> - <p>Download archive files just here:</p> - </div> - <div class="col"> - <a (click)="downloadArchive()" class="btn btn-outline-primary" title="Download an archive with all files"> - <span class="fas fa-archive"></span> Files archive - </a> - </div> - </div> - </div> - </accordion-group> -</accordion> diff --git a/client/src/app/instance/search/components/result/download.component.spec.ts b/client/src/app/instance/search/components/result/download.component.spec.ts deleted file mode 100644 index c6a1b42dca00844985d60e2c5e0613795c80d4e5..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/result/download.component.spec.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; - -import { AccordionModule } from 'ngx-bootstrap/accordion'; - -import { DownloadComponent } from './download.component'; -import { AppConfigService } from '../../../../app-config.service'; - -describe('[Instance][Search][Component][Result] DownloadComponent', () => { - let component: DownloadComponent; - let fixture: ComponentFixture<DownloadComponent>; - let appConfigServiceStub = new AppConfigService(); - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [DownloadComponent], - imports: [ - HttpClientTestingModule, - AccordionModule.forRoot(), - BrowserAnimationsModule - ], - providers: [{ provide: AppConfigService, useValue: appConfigServiceStub }] - }); - fixture = TestBed.createComponent(DownloadComponent); - component = fixture.componentInstance; - }); - - it('should create the component', () => { - expect(component).toBeTruthy(); - }); - - it('#isDownloadActivated() should return if download section has to be enabled or not', () => { - component.datasetList = [ - { - name: 'myDataset', - table_ref: 'table', - label: 'my dataset', - description: 'This is my dataset', - display: 1, - data_path: '/path', - survey_name: 'mySurvey', - id_dataset_family: 1, - public: true, - full_data_path: '/data/path', - info_survey_enabled: true, - info_survey_label: 'More about this survey', - cone_search_enabled: true, - cone_search_opened: true, - cone_search_column_ra: 1, - cone_search_column_dec: 2, - cone_search_plot_enabled: false, - download_enabled: true, - download_opened: true, - download_json: true, - download_csv: true, - download_ascii: true, - download_vo: true, - download_archive: true, - summary_enabled: false, - summary_opened: false, - server_link_enabled: false, - server_link_opened: true, - datatable_enabled: true, - datatable_opened: true, - datatable_selectable_rows: true - }, - { - name: 'anotherDataset', - table_ref: 'table', - label: 'another dataset', - description: 'This is another dataset', - display: 1, - data_path: '/path', - survey_name: 'mySurvey', - id_dataset_family: 1, - public: true, - full_data_path: '/data/path', - info_survey_enabled: true, - info_survey_label: 'More about this survey', - cone_search_enabled: true, - cone_search_opened: true, - cone_search_column_ra: 1, - cone_search_column_dec: 2, - cone_search_plot_enabled: false, - download_enabled: false, - download_opened: false, - download_json: true, - download_csv: false, - download_ascii: false, - download_vo: false, - download_archive: false, - summary_enabled: true, - summary_opened: true, - server_link_enabled: true, - server_link_opened: true, - datatable_enabled: true, - datatable_opened: true, - datatable_selectable_rows: true - } - ]; - component.datasetSelected = 'myDataset'; - expect(component.isDownloadActivated()).toBeTruthy(); - component.datasetSelected = 'anotherDataset'; - expect(component.isDownloadActivated()).toBeFalsy(); - }); - - it('#isDownloadOpened() should return if download tab has to be opened or not', () => { - component.datasetList = [ - { - name: 'myDataset', - table_ref: 'table', - label: 'my dataset', - description: 'This is my dataset', - display: 1, - data_path: '/path', - survey_name: 'mySurvey', - id_dataset_family: 1, - public: true, - full_data_path: '/data/path', - info_survey_enabled: true, - info_survey_label: 'More about this survey', - cone_search_enabled: true, - cone_search_opened: true, - cone_search_column_ra: 1, - cone_search_column_dec: 2, - cone_search_plot_enabled: false, - download_enabled: true, - download_opened: true, - download_json: true, - download_csv: true, - download_ascii: true, - download_vo: true, - download_archive: true, - summary_enabled: false, - summary_opened: false, - server_link_enabled: false, - server_link_opened: false, - datatable_enabled: true, - datatable_opened: true, - datatable_selectable_rows: true - }, - { - name: 'anotherDataset', - table_ref: 'table', - label: 'another dataset', - description: 'This is another dataset', - display: 1, - data_path: '/path', - survey_name: 'mySurvey', - id_dataset_family: 1, - public: true, - full_data_path: '/data/path', - info_survey_enabled: true, - info_survey_label: 'More about this survey', - cone_search_enabled: true, - cone_search_opened: true, - cone_search_column_ra: 1, - cone_search_column_dec: 2, - cone_search_plot_enabled: false, - download_enabled: false, - download_opened: false, - download_json: true, - download_csv: false, - download_ascii: false, - download_vo: false, - download_archive: false, - summary_enabled: true, - summary_opened: true, - server_link_enabled: true, - server_link_opened: true, - datatable_enabled: true, - datatable_opened: true, - datatable_selectable_rows: true - } - ]; - component.datasetSelected = 'myDataset'; - expect(component.isDownloadOpened()).toBeTruthy(); - component.datasetSelected = 'anotherDataset'; - expect(component.isDownloadOpened()).toBeFalsy(); - }); - - it('#getConfigDownloadResultFormat() should return if download button for the given format has to be displayed', () => { - component.datasetList = [ - { - name: 'myDataset', - table_ref: 'table', - label: 'my dataset', - description: 'This is my dataset', - display: 1, - data_path: '/path', - survey_name: 'mySurvey', - id_dataset_family: 1, - public: true, - full_data_path: '/data/path', - info_survey_enabled: true, - info_survey_label: 'More about this survey', - cone_search_enabled: true, - cone_search_opened: true, - cone_search_column_ra: 1, - cone_search_column_dec: 2, - cone_search_plot_enabled: false, - download_enabled: true, - download_opened: true, - download_json: true, - download_csv: true, - download_ascii: false, - download_vo: true, - download_archive: true, - summary_enabled: false, - summary_opened: false, - server_link_enabled: false, - server_link_opened: false, - datatable_enabled: true, - datatable_opened: true, - datatable_selectable_rows: true - }, - { - name: 'anotherDataset', - table_ref: 'table', - label: 'another dataset', - description: 'This is another dataset', - display: 1, - data_path: '/path', - survey_name: 'mySurvey', - id_dataset_family: 1, - public: true, - full_data_path: '/data/path', - info_survey_enabled: true, - info_survey_label: 'More about this survey', - cone_search_enabled: true, - cone_search_opened: true, - cone_search_column_ra: 1, - cone_search_column_dec: 2, - cone_search_plot_enabled: false, - download_enabled: false, - download_opened: false, - download_json: true, - download_csv: false, - download_ascii: false, - download_vo: false, - download_archive: false, - summary_enabled: true, - summary_opened: true, - server_link_enabled: true, - server_link_opened: true, - datatable_enabled: true, - datatable_opened: true, - datatable_selectable_rows: true - } - ]; - component.datasetSelected = 'myDataset'; - expect(component.getConfigDownloadResultFormat('download_csv')).toBeTruthy(); - expect(component.getConfigDownloadResultFormat('download_ascii')).toBeFalsy(); - }); -}); diff --git a/client/src/app/instance/search/components/result/download.component.ts b/client/src/app/instance/search/components/result/download.component.ts deleted file mode 100644 index 0beeeb88810888111b1a90aa48eee9f9dd28feda..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/result/download.component.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, EventEmitter, Input, Output } from '@angular/core'; - -import { Criterion, ConeSearch } from '../../../store/models'; -import { Dataset, Attribute } from 'src/app/metamodel/models'; - -/** - * @class - * @classdesc Search result download component. - */ -@Component({ - selector: 'app-download', - templateUrl: 'download.component.html' -}) -export class DownloadComponent { - @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(); - @Output() startTaskCreateResult: EventEmitter<{ format: string, selectedData: boolean, broadcastVo: boolean }> = new EventEmitter(); - @Output() startTaskCreateArchive: EventEmitter<{ selectedData: boolean }> = new EventEmitter(); - - /** - * Checks if download tab has to be display. - * - * @return boolean - */ - isDownloadActivated(): boolean { - return this.datasetList.find(d => d.name === this.datasetSelected).download_enabled; - } - - /** - * Checks if download tab has to be open. - * - * @return boolean - */ - isDownloadOpened(): boolean { - return this.datasetList.find(d => d.name === this.datasetSelected).download_opened; - } - - /** - * 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 { - return this.datasetList.find(d => d.name === this.datasetSelected)[format]; - } - - isArchiveIsAvailable() { - return this.attributeList - .filter(attribute => this.outputList.includes(attribute.id)) - .filter(attribute => attribute.archive) - .length > 0; - } - - downloadResult(format: string) { - this.startTaskCreateResult.emit({ - format, - selectedData: false, - broadcastVo: false - }); - } - - broadcastResult() { - this.startTaskCreateResult.emit({ - format: 'votable', - selectedData: false, - broadcastVo: true - }) - } - - downloadArchive() { - this.startTaskCreateArchive.emit({ - selectedData: false - }); - } -} diff --git a/client/src/app/instance/search/components/result/image-list-result.component.html b/client/src/app/instance/search/components/result/image-list-result.component.html new file mode 100644 index 0000000000000000000000000000000000000000..f6f9ef6d245fc966aa2c963051994be0d5220800 --- /dev/null +++ b/client/src/app/instance/search/components/result/image-list-result.component.html @@ -0,0 +1,38 @@ +<app-spinner *ngIf="imageListIsLoading"></app-spinner> + +<div *ngIf="imageListIsLoaded" class="row"> + <div *ngFor="let image of imageList" class="col-2"> + <div class="row"> + <span class="pointer"> + <img (click)="openConeSearch(cs, getHref(image))" [src]="getHref(image)" height="100px" class="img-thumbnail mb-2" /> + </span> + </div> + <div class="row justify-content-center"> + <a class="btn btn-outline-primary btn-sm" [href]="getFitsCutUrl(image)" (click)="saveFitsCutFile($event, image)"> + <span class="fa-solid fa-floppy-disk"></span> + </a> + + <a class="btn btn-outline-primary btn-sm" [class.disabled]="!sampRegistered" (click)="broadcast(image)"> + <span class="fas fa-broadcast-tower"></span> + </a> + </div> + </div> +</div> + +<ng-template #cs> + <div class="modal-header"> + <h4 class="modal-title pull-left">Cone-search</h4> + <button type="button" class="close pull-right" aria-label="Close" (click)="modalRef.hide()"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <app-spinner *ngIf="dataIsLoading"></app-spinner> + <app-cone-search-plot *ngIf="dataIsLoaded" + [coneSearch]="coneSearch" + [dataset]="dataset" + [data]="getData()" + [backgroundHref]="backgroundHref"> + </app-cone-search-plot> + </div> +</ng-template> \ No newline at end of file diff --git a/client/src/app/instance/search/components/result/image-list-result.component.ts b/client/src/app/instance/search/components/result/image-list-result.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed7b109c5714ede334f00dd85a7aff249760be16 --- /dev/null +++ b/client/src/app/instance/search/components/result/image-list-result.component.ts @@ -0,0 +1,78 @@ +import { Component, Input, Output, EventEmitter, TemplateRef } from '@angular/core'; + +import FileSaver from 'file-saver'; +import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal'; + +import { Dataset, Attribute, Image } from 'src/app/metamodel/models'; +import { ConeSearch } from 'src/app/instance/store/models'; +import { AppConfigService } from 'src/app/app-config.service'; + +@Component({ + selector: 'app-image-list-result', + templateUrl: 'image-list-result.component.html' +}) +export class ImageListResultComponent { + @Input() dataset: Dataset; + @Input() attributeList: Attribute[]; + @Input() coneSearch: ConeSearch; + @Input() data: any; + @Input() dataIsLoading: boolean; + @Input() dataIsLoaded: boolean; + @Input() imageList: Image[]; + @Input() imageListIsLoading: boolean; + @Input() imageListIsLoaded: boolean; + @Input() sampRegistered: boolean; + @Output() broadcastImage: EventEmitter<string> = new EventEmitter(); + @Output() downloadFile: EventEmitter<{url: string, filename: string}> = new EventEmitter(); + + modalRef: BsModalRef; + backgroundHref: string; + + constructor(private config: AppConfigService, private modalService: BsModalService) { } + + getHref(image: Image) { + let href = `${this.config.servicesUrl}/fits-cut-to-png/${this.dataset.name}?filename=${image.file_path}`; + href += `&ra=${this.coneSearch.ra}`; + href += `&dec=${this.coneSearch.dec}`; + href += `&radius=${this.coneSearch.radius}`; + href += `&stretch=${image.stretch}`; + href += `&pmin=${image.pmin}`; + href += `&pmax=${image.pmax}`; + href += `&axes=false`; + return href; + } + + getFitsCutUrl(image: Image) { + let url = `${this.config.servicesUrl}/fits-cut/${this.dataset.name}?filename=${image.file_path}`; + url += `&ra=${this.coneSearch.ra}`; + url += `&dec=${this.coneSearch.dec}`; + url += `&radius=${this.coneSearch.radius}`; + return url; + } + + saveFitsCutFile(event, image: Image) { + event.preventDefault(); + + const url = this.getFitsCutUrl(image); + const filename = image.file_path.substring(image.file_path.lastIndexOf('/') + 1); + + this.downloadFile.emit({url, filename}); + } + + broadcast(image: Image) { + this.broadcastImage.emit( + this.getFitsCutUrl(image) + ); + } + + getData() { + const columnRa = this.attributeList.find(a => a.id === this.dataset.cone_search_column_ra); + const columnDec = this.attributeList.find(a => a.id === this.dataset.cone_search_column_dec); + return this.data.map(d => ({ "x": +d[columnRa.label], "y": +d[columnDec.label] })); + } + + openConeSearch(template: TemplateRef<any>, backgroundHref: string): void { + this.backgroundHref = backgroundHref; + this.modalRef = this.modalService.show(template, { class: 'modal-lg' }); + } +} \ No newline at end of file diff --git a/client/src/app/instance/search/components/result/index.ts b/client/src/app/instance/search/components/result/index.ts index 80f3a16b03bf6dc2754b81ce5477fa14265ebdd6..550b053fa80c26002e2a762d39c89b3e7f27b735 100644 --- a/client/src/app/instance/search/components/result/index.ts +++ b/client/src/app/instance/search/components/result/index.ts @@ -1,21 +1,17 @@ -import { DatatableTabComponent } from './datatable-tab.component'; -import { DownloadComponent } from './download.component'; -import { ReminderComponent } from './reminder.component'; +import { DownloadResultComponent } from './download-result.component'; +import { ImageListResultComponent } from './image-list-result.component'; +import { DatatableActionsComponent } from './datatable-actions.component'; import { UrlDisplayComponent } from './url-display.component'; import { DatatableComponent } from './datatable.component'; -import { DatatableActionsComponent } from './datatable-actions.component'; -import { DownloadFileTabComponent } from './download-file-tab.component'; import { ConeSearchPlotComponent } from './cone-search-plot.component'; import { rendererComponents } from './renderer'; export const resultComponents = [ - DatatableTabComponent, - DownloadComponent, - ReminderComponent, UrlDisplayComponent, + ImageListResultComponent, DatatableComponent, DatatableActionsComponent, - DownloadFileTabComponent, ConeSearchPlotComponent, + DownloadResultComponent, rendererComponents ]; diff --git a/client/src/app/instance/search/components/result/reminder.component.html b/client/src/app/instance/search/components/result/reminder.component.html deleted file mode 100644 index 38372acef640c24c36328c786925527dd5ecfd8f..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/result/reminder.component.html +++ /dev/null @@ -1,78 +0,0 @@ -<accordion *ngIf="isSummaryActivated()" [isAnimated]="true"> - <accordion-group #ag [isOpen]="isSummaryOpened()" [panelClass]="'custom-accordion'" class="my-2"> - <button class="btn btn-link btn-block clearfix" accordion-heading> - <span class="pull-left float-left"> - Search summary - - <span *ngIf="ag.isOpen"> - <span class="fas fa-chevron-up"></span> - </span> - <span *ngIf="!ag.isOpen"> - <span class="fas fa-chevron-down"></span> - </span> - </span> - </button> - <div> - <tabset [justified]="true"> - <tab> - <ng-template tabHeading> - Criteria <span class="badge badge-pill badge-secondary">{{ nbCriteria() }}</span> - </ng-template> - <div class="tab-content container-fluid pt-3"> - <div *ngIf="nbCriteria() === 0" class="text-center font-weight-bold pt-5"> - No selected criteria - </div> - - <div *ngIf="nbCriteria() > 0" class="row"> - <div *ngIf="coneSearch" class="col-12 col-md-6 col-xl-4 pb-3"> - <span class="title">Cone search</span> - <ul class="list-unstyled pl-3"> - <li>RA = {{ coneSearch.ra }}°</li> - <li>DEC = {{ coneSearch.dec }}°</li> - <li>radius = {{ coneSearch.radius }} arcsecond</li> - </ul> - </div> - - <ng-container *ngFor="let family of criteriaFamilyList"> - <ng-container *ngIf="criteriaByFamily(family.id).length > 0"> - <div class="col-12 col-md-6 col-xl-4 pb-3"> - <span class="title">{{ family.label }}</span> - <ul class="list-unstyled pl-3"> - <li *ngFor="let criterion of criteriaByFamily(family.id)"> - {{ getAttribute(criterion.id).form_label }} {{ printCriterion(criterion) }} - </li> - </ul> - </div> - </ng-container> - </ng-container> - </div> - </div> - </tab> - <tab> - <ng-template tabHeading> - Outputs <span class="badge badge-pill badge-secondary">{{ outputList.length }}</span> - </ng-template> - <div class="tab-content container-fluid pt-3"> - <div class="row"> - <ng-container *ngFor="let family of outputFamilyList"> - <ng-container *ngFor="let category of categoryListByFamily(family.id)"> - <ng-container *ngIf="outputListByCategory(category.id).length > 0"> - <div class="col-12 col-md-6 col-lg-4 col-xl-3 pb-3"> - <span class="title">{{ family.label }}</span><br> - <span class="title pl-3">{{ category.label }}</span> - <ul class="list-unstyled pl-5"> - <li *ngFor="let output of outputListByCategory(category.id)"> - {{ getAttribute(output).form_label }} - </li> - </ul> - </div> - </ng-container> - </ng-container> - </ng-container> - </div> - </div> - </tab> - </tabset> - </div> - </accordion-group> -</accordion> diff --git a/client/src/app/instance/search/components/result/reminder.component.scss b/client/src/app/instance/search/components/result/reminder.component.scss deleted file mode 100644 index 2ae066f52c19712023452598c1cdfb6fab911bd8..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/result/reminder.component.scss +++ /dev/null @@ -1,21 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -.tab-content { - height: 250px; - border-bottom: #dee2e6 solid 1px; - border-left: #dee2e6 solid 1px; - border-right: #dee2e6 solid 1px; - overflow-y: auto; -} - -.title { - color: #6c757d; - font-size: 90%; -} diff --git a/client/src/app/instance/search/components/result/reminder.component.spec.ts b/client/src/app/instance/search/components/result/reminder.component.spec.ts deleted file mode 100644 index 727252824e92e5cc720799d2a7fcb9d082b8f7ca..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/result/reminder.component.spec.ts +++ /dev/null @@ -1,454 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { AccordionModule } from 'ngx-bootstrap/accordion'; - -import { ReminderComponent } from './reminder.component'; -import { FieldCriterion } from '../../../store/models/criterion'; -import { TabsModule } from 'ngx-bootstrap/tabs'; -import { Criterion } from '../../../store/models'; - -describe('[Instance][Search][Component][Result] ReminderComponent', () => { - let component: ReminderComponent; - let fixture: ComponentFixture<ReminderComponent>; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ReminderComponent], - imports: [ - AccordionModule.forRoot(), - TabsModule.forRoot(), - BrowserAnimationsModule - ] - }); - fixture = TestBed.createComponent(ReminderComponent); - component = fixture.componentInstance; - }); - - it('should create the component', () => { - expect(component).toBeTruthy(); - }); - - it('#isSummaryActivated() should return if summary has to be enabled or not', () => { - component.datasetList = [ - { - name: 'myDataset', - table_ref: 'table', - label: 'my dataset', - description: 'This is my dataset', - display: 1, - data_path: '/path', - survey_name: 'mySurvey', - id_dataset_family: 1, - public: true, - full_data_path: '/data/path', - info_survey_enabled: true, - info_survey_label: 'More about this survey', - cone_search_enabled: true, - cone_search_opened: true, - cone_search_column_ra: 1, - cone_search_column_dec: 2, - cone_search_plot_enabled: false, - download_enabled: true, - download_opened: true, - download_json: true, - download_csv: true, - download_ascii: true, - download_vo: true, - download_archive: true, - summary_enabled: false, - summary_opened: false, - server_link_enabled: false, - server_link_opened: true, - datatable_enabled: true, - datatable_opened: true, - datatable_selectable_rows: true - }, - { - name: 'anotherDataset', - table_ref: 'table', - label: 'another dataset', - description: 'This is another dataset', - display: 1, - data_path: '/path', - survey_name: 'mySurvey', - id_dataset_family: 1, - public: true, - full_data_path: '/data/path', - info_survey_enabled: true, - info_survey_label: 'More about this survey', - cone_search_enabled: true, - cone_search_opened: true, - cone_search_column_ra: 1, - cone_search_column_dec: 2, - cone_search_plot_enabled: false, - download_enabled: true, - download_opened: true, - download_json: true, - download_csv: true, - download_ascii: true, - download_vo: true, - download_archive: true, - summary_enabled: true, - summary_opened: true, - server_link_enabled: true, - server_link_opened: true, - datatable_enabled: true, - datatable_opened: true, - datatable_selectable_rows: true - } - ]; - component.datasetSelected = 'myDataset'; - expect(component.isSummaryActivated()).toBeFalsy(); - component.datasetSelected = 'anotherDataset'; - expect(component.isSummaryActivated()).toBeTruthy(); - }); - - it('#isSummaryOpened() should return if summary tab has to be opened or not', () => { - component.datasetList = [ - { - name: 'myDataset', - table_ref: 'table', - label: 'my dataset', - description: 'This is my dataset', - display: 1, - data_path: '/path', - survey_name: 'mySurvey', - id_dataset_family: 1, - public: true, - full_data_path: '/data/path', - info_survey_enabled: true, - info_survey_label: 'More about this survey', - cone_search_enabled: true, - cone_search_opened: true, - cone_search_column_ra: 1, - cone_search_column_dec: 2, - cone_search_plot_enabled: false, - download_enabled: true, - download_opened: true, - download_json: true, - download_csv: true, - download_ascii: true, - download_vo: true, - download_archive: true, - summary_enabled: false, - summary_opened: false, - server_link_enabled: false, - server_link_opened: false, - datatable_enabled: true, - datatable_opened: true, - datatable_selectable_rows: true - }, - { - name: 'anotherDataset', - table_ref: 'table', - label: 'another dataset', - description: 'This is another dataset', - display: 1, - data_path: '/path', - survey_name: 'mySurvey', - id_dataset_family: 1, - public: true, - full_data_path: '/data/path', - info_survey_enabled: true, - info_survey_label: 'More about this survey', - cone_search_enabled: true, - cone_search_opened: true, - cone_search_column_ra: 1, - cone_search_column_dec: 2, - cone_search_plot_enabled: false, - download_enabled: true, - download_opened: true, - download_json: true, - download_csv: true, - download_ascii: true, - download_vo: true, - download_archive: true, - summary_enabled: true, - summary_opened: true, - server_link_enabled: true, - server_link_opened: true, - datatable_enabled: true, - datatable_opened: true, - datatable_selectable_rows: true - } - ]; - component.datasetSelected = 'myDataset'; - expect(component.isSummaryOpened()).toBeFalsy(); - component.datasetSelected = 'anotherDataset'; - expect(component.isSummaryOpened()).toBeTruthy(); - }); - - it('#nbCriteria() should return criteria amount', () => { - component.criteriaList = []; - expect(component.nbCriteria()).toEqual(0); - component.coneSearch = { ra: 1, dec: 2, radius: 3 }; - expect(component.nbCriteria()).toEqual(1); - }); - - it('#criteriaByFamily() should return criteria for the given family', () => { - component.attributeList = [ - { - id: 2, - name: 'name_two', - label: 'label_two', - form_label: 'form_label_two', - description : 'description_two', - primary_key: false, - type: '', - search_type : 'field', - operator : '=', - min: null, - max: null, - placeholder_min: null, - placeholder_max: null, - criteria_display: 1, - output_display: 1, - selected: true, - renderer: null, - renderer_config: null, - order_by: true, - archive: false, - detail: true, - display_detail: 1, - renderer_detail: null, - renderer_detail_config: null, - options: null, - vo_utype: null, - vo_ucd: null, - vo_unit: null, - vo_description: null, - vo_datatype: null, - vo_size: null, - id_criteria_family: 2, - id_output_category: null - }, - { - id: 1, - name: 'name_one', - label: 'label_one', - form_label: 'form_label_one', - description: 'description_one', - primary_key: true, - type: 'integer', - search_type: 'field', - operator: '=', - min: null, - max: null, - placeholder_min: null, - placeholder_max: null, - criteria_display: 2, - output_display: 2, - selected: true, - renderer: null, - renderer_config: null, - order_by: true, - archive: false, - detail: true, - display_detail: 2, - renderer_detail: null, - renderer_detail_config: null, - options: null, - vo_utype: null, - vo_ucd: null, - vo_unit: null, - vo_description: null, - vo_datatype: null, - vo_size: null, - id_criteria_family: 1, - id_output_category: null - } - ]; - component.criteriaList = [ - {'id':1,'type':'field','operator':'eq','value':'one'} as FieldCriterion, - {'id':2,'type':'field','operator':'eq','value':'two'} as FieldCriterion - ]; - expect(component.criteriaByFamily(1).length).toEqual(1); - expect(component.criteriaByFamily(1)[0].id).toEqual(1); - }); - - it('#getAttribute() should return the attribute with the given id', () => { - component.attributeList = [ - { - id: 2, - name: 'name_two', - label: 'label_two', - form_label: 'form_label_two', - description : 'description_two', - primary_key: false, - type: '', - search_type : 'field', - operator : '=', - min: null, - max: null, - placeholder_min: null, - placeholder_max: null, - criteria_display: 1, - output_display: 1, - selected: true, - renderer: null, - renderer_config: null, - order_by: true, - archive: false, - detail: true, - display_detail: 1, - renderer_detail: null, - renderer_detail_config: null, - options: null, - vo_utype: null, - vo_ucd: null, - vo_unit: null, - vo_description: null, - vo_datatype: null, - vo_size: null, - id_criteria_family: null, - id_output_category: null - }, - { - id: 1, - name: 'name_one', - label: 'label_one', - form_label: 'form_label_one', - description: 'description_one', - primary_key: true, - type: 'integer', - search_type: 'field', - operator: '=', - min: null, - max: null, - placeholder_min: null, - placeholder_max: null, - criteria_display: 2, - output_display: 2, - selected: true, - renderer: null, - renderer_config: null, - order_by: true, - archive: false, - detail: true, - display_detail: 2, - renderer_detail: null, - renderer_detail_config: null, - options: null, - vo_utype: null, - vo_ucd: null, - vo_unit: null, - vo_description: null, - vo_datatype: null, - vo_size: null, - id_criteria_family: null, - id_output_category: null - } - ]; - expect(component.getAttribute(1).name).toBe('name_one'); - }); - - it('#printCriterion() should return pretty criterion', () => { - const criterion: Criterion = {'id':1,'type':'field','operator':'eq','value':'one'} as FieldCriterion; - expect(component.printCriterion(criterion)).toBe('= one'); - }); - - it('#categoryListByFamily() should return output categories for the given output family', () => { - component.outputCategoryList = [ - { - id: 3, - label: 'The last output category', - display: 30, - id_output_family: 2 - }, - { - id: 1, - label: 'Another output category', - display: 10, - id_output_family: 1 - }, - { - id: 2, - label: 'Default output category', - display: 20, - id_output_family: 1 - } - ]; - expect(component.categoryListByFamily(1).length).toEqual(2); - expect(component.categoryListByFamily(1)[0].id).toEqual(1); - expect(component.categoryListByFamily(1)[1].id).toEqual(2); - }); - - it('#outputListByCategory() should return outputs for the given output category', () => { - component.attributeList = [ - { - id: 2, - name: 'name_two', - label: 'label_two', - form_label: 'form_label_two', - description : 'description_two', - primary_key: false, - type: '', - search_type : 'field', - operator : '=', - min: null, - max: null, - placeholder_min: null, - placeholder_max: null, - criteria_display: 1, - output_display: 1, - selected: true, - renderer: null, - renderer_config: null, - order_by: true, - archive: false, - detail: true, - display_detail: 1, - renderer_detail: null, - renderer_detail_config: null, - options: null, - vo_utype: null, - vo_ucd: null, - vo_unit: null, - vo_description: null, - vo_datatype: null, - vo_size: null, - id_criteria_family: null, - id_output_category: 2 - }, - { - id: 1, - name: 'name_one', - label: 'label_one', - form_label: 'form_label_one', - description: 'description_one', - primary_key: true, - type: 'integer', - search_type: 'field', - operator: '=', - min: null, - max: null, - placeholder_min: null, - placeholder_max: null, - criteria_display: 2, - output_display: 2, - selected: true, - renderer: null, - renderer_config: null, - order_by: true, - archive: false, - detail: true, - display_detail: 2, - renderer_detail: null, - renderer_detail_config: null, - options: null, - vo_utype: null, - vo_ucd: null, - vo_unit: null, - vo_description: null, - vo_datatype: null, - vo_size: null, - id_criteria_family: null, - id_output_category: 1 - } - ]; - component.outputList = [1, 2]; - expect(component.outputListByCategory(1).length).toEqual(1); - expect(component.outputListByCategory(1)[0]).toEqual(1); - }); -}); diff --git a/client/src/app/instance/search/components/result/reminder.component.ts b/client/src/app/instance/search/components/result/reminder.component.ts deleted file mode 100644 index 71c94e903e64a2e33d28b4c0bf16f2e7b24b6d30..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/result/reminder.component.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input } from '@angular/core'; - -import { Criterion, ConeSearch, getPrettyCriterion } from 'src/app/instance/store/models'; -import { Dataset, Attribute, CriteriaFamily, OutputFamily, OutputCategory } from 'src/app/metamodel/models'; - -/** - * @class - * @classdesc Search result reminder component. - */ -@Component({ - selector: 'app-reminder', - templateUrl: 'reminder.component.html', - styleUrls: ['reminder.component.scss'] -}) -export class ReminderComponent { - @Input() datasetSelected: string; - @Input() datasetList: Dataset[]; - @Input() attributeList: Attribute[]; - @Input() criteriaFamilyList: CriteriaFamily[]; - @Input() outputFamilyList: OutputFamily[]; - @Input() outputCategoryList: OutputCategory[]; - @Input() criteriaList: Criterion[]; - @Input() coneSearch: ConeSearch; - @Input() outputList: number[]; - - /** - * Checks if reminder has to be display. - * - * @return boolean - */ - isSummaryActivated(): boolean { - return this.datasetList.find(d => d.name === this.datasetSelected).summary_enabled; - } - - /** - * Checks if reminder has to be open. - * - * @return boolean - */ - isSummaryOpened(): boolean { - return this.datasetList.find(d => d.name === this.datasetSelected).summary_opened; - } - - /** - * Returns total of added criteria. - * - * @return number - */ - nbCriteria(): number { - if (this.coneSearch) { - return this.criteriaList.length + 1; - } - return this.criteriaList.length; - } - - /** - * Returns criteria list for the given criteria family ID. - * - * @param {number} idFamily - The criteria family ID. - * - * @return Criterion[] - */ - criteriaByFamily(idFamily: number): Criterion[] { - const attributeListByFamily: Attribute[] = this.attributeList - .filter(attribute => attribute.id_criteria_family === idFamily); - return this.criteriaList - .filter(criterion => attributeListByFamily.includes( - this.attributeList.find(attribute => attribute.id === criterion.id)) - ); - } - - /** - * Returns attribute for the given attribute ID. - * - * @param {number} id - The attribute ID. - * - * @return Attribute - */ - getAttribute(id: number): Attribute { - return this.attributeList.find(attribute => attribute.id === id); - } - - /** - * Returns criterion pretty printed. - * - * @param {Criterion} criterion - The criterion. - * - * @return string - */ - printCriterion(criterion: Criterion): string { - return getPrettyCriterion(criterion); - } - - /** - * Returns output category list for the given criteria family ID. - * - * @param {number} idFamily - The criteria family ID. - * - * @return OutputCategory[] - */ - categoryListByFamily(idFamily: number): OutputCategory[] { - return this.outputCategoryList.filter(outputCategory => outputCategory.id_output_family === idFamily); - } - - /** - * Returns output list for the given category ID. - * - * @param {number} idCategory - The output category ID. - * - * @return number[] - */ - outputListByCategory(idCategory: number): number[] { - const attributeListByCategory: Attribute[] = this.attributeList - .filter(attribute => attribute.id_output_category === idCategory); - return this.outputList - .filter(output => attributeListByCategory.includes( - this.attributeList.find(attribute => attribute.id === output)) - ); - } -} diff --git a/client/src/app/instance/search/components/result/renderer/download-renderer.component.ts b/client/src/app/instance/search/components/result/renderer/download-renderer.component.ts index e2cfc9a344717640c30fa5c6dbfd2aee6b69d3ef..348b909cdd9f01cbeb0988790473564c29cc4ec0 100644 --- a/client/src/app/instance/search/components/result/renderer/download-renderer.component.ts +++ b/client/src/app/instance/search/components/result/renderer/download-renderer.component.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; +import { Component, Input, ChangeDetectionStrategy, Output, EventEmitter } from '@angular/core'; import { DownloadRendererConfig } from 'src/app/metamodel/models/renderers/download-renderer-config.model'; import { getHost } from 'src/app/shared/utils'; @@ -27,7 +27,7 @@ export class DownloadRendererComponent { @Input() datasetName: string; @Input() datasetPublic: boolean; @Input() config: DownloadRendererConfig; - @Output() downloadFile: EventEmitter<{url: string, fileId: string, datasetName: string, filename: string}> = new EventEmitter(); + @Output() downloadFile: EventEmitter<{url: string, filename: string}> = new EventEmitter(); constructor(private appConfig: AppConfigService) { } @@ -62,15 +62,6 @@ export class DownloadRendererComponent { const url = this.getHref(); const filename = url.substring(url.lastIndexOf('/') + 1); - const n = Math.floor(Math.random() * 11); - const k = Math.floor(Math.random() * 1000000); - const m = String.fromCharCode(n) + k; - - this.downloadFile.emit({ - url, - fileId: m, - datasetName: this.datasetName, - filename - }); + this.downloadFile.emit({ url, filename }); } } diff --git a/client/src/app/instance/search/components/result/url-display.component.html b/client/src/app/instance/search/components/result/url-display.component.html index 109b122d3e7c0438f942653b9b75497d43986112..38d1db788b13717e658ad1577b3352f130c00e3f 100644 --- a/client/src/app/instance/search/components/result/url-display.component.html +++ b/client/src/app/instance/search/components/result/url-display.component.html @@ -1,29 +1,3 @@ -<accordion *ngIf="urlDisplayEnabled()" [isAnimated]="true"> - <accordion-group #ag [isOpen]="urlDisplayOpened()" [panelClass]="'custom-accordion'" class="my-2"> - <button class="btn btn-link btn-block clearfix" accordion-heading> - <span class="pull-left float-left"> - Direct link to the result (JSON) - - <span *ngIf="ag.isOpen"> - <span class="fas fa-chevron-up"></span> - </span> - <span *ngIf="!ag.isOpen"> - <span class="fas fa-chevron-down"></span> - </span> - </span> - </button> - <div> - <div class="row"> - <div class="col"> - <a target="_blank" [href]="getUrl()">{{ getUrl() }}</a> - </div> - <div class="col-2 align-self-center text-center"> - <button class="btn btn-sm btn-outline-primary" (click)="copyToClipboard()" - title="Copy url to clipboard"> - COPY - </button> - </div> - </div> - </div> - </accordion-group> -</accordion> +<div *ngIf="dataset.server_link_enabled" class="text-right mb-2"> + Direct link to the result (JSON): <a target="_blank" [href]="getUrl('json')">{{ getUrl('json') }}</a> +</div> diff --git a/client/src/app/instance/search/components/result/url-display.component.ts b/client/src/app/instance/search/components/result/url-display.component.ts index ff9cd13c5bda793b49f1a852974b56a042186676..53688b2fff27a184e5372ea59dcb50b364a456e7 100644 --- a/client/src/app/instance/search/components/result/url-display.component.ts +++ b/client/src/app/instance/search/components/result/url-display.component.ts @@ -7,14 +7,9 @@ * file that was distributed with this source code. */ -import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; -import { ToastrService } from 'ngx-toastr'; - -import { Criterion, ConeSearch, criterionToString } from 'src/app/instance/store/models'; -import { Dataset } from 'src/app/metamodel/models'; -import { getHost } from 'src/app/shared/utils'; -import { AppConfigService } from 'src/app/app-config.service'; +import { AbstractDownloadComponent } from './abstract-download.component'; /** * @class @@ -25,59 +20,4 @@ import { AppConfigService } from 'src/app/app-config.service'; templateUrl: 'url-display.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class UrlDisplayComponent { - @Input() datasetSelected: string; - @Input() datasetList: Dataset[]; - @Input() criteriaList: Criterion[]; - @Input() outputList: number[]; - @Input() coneSearch: ConeSearch; - - constructor(private toastr: ToastrService, private appConfig: AppConfigService) { } - - /** - * Checks if URL display is enabled. - * - * @return boolean - */ - urlDisplayEnabled(): boolean { - return this.datasetList.find(d => d.name === this.datasetSelected).server_link_enabled; - } - - /** - * Checks if URL tab has to be opened. - * - * @return boolean - */ - urlDisplayOpened(): boolean { - return this.datasetList.find(d => d.name === this.datasetSelected).server_link_opened; - } - - /** - * Returns API URL to get data with user parameters. - * - * @return string - */ - getUrl(): 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(';')}`; - } - if (this.coneSearch) { - query += `&cs=${this.coneSearch.ra}:${this.coneSearch.dec}:${this.coneSearch.radius}`; - } - return query; - } - - /** - * Copies API URL to user clipboard. - */ - copyToClipboard(): void { - const selBox = document.createElement('textarea'); - selBox.value = this.getUrl(); - document.body.appendChild(selBox); - selBox.select(); - document.execCommand('copy'); - document.body.removeChild(selBox); - this.toastr.success('Copied'); - } -} +export class UrlDisplayComponent extends AbstractDownloadComponent { } diff --git a/client/src/app/instance/search/containers/result.component.html b/client/src/app/instance/search/containers/result.component.html index cb01ac25c98cb0ff8c8f0c56a97ebd7920a2b9a0..f375f3fe162d395930fa84f79f2ba9ff4500741b 100644 --- a/client/src/app/instance/search/containers/result.component.html +++ b/client/src/app/instance/search/containers/result.component.html @@ -19,41 +19,71 @@ </div> </div> <ng-container *ngIf="(dataLength | async) > 0"> - <div class="jumbotron mb-4 py-4"> - <div class="lead"> - Dataset <span class="font-weight-bold">{{ (datasetList | async | datasetByName:(datasetSelected | async)).label }}</span> - selected with <span class="font-weight-bold">{{ dataLength | async }}</span> objects found. + <div class="jumbotron row mb-4 py-4"> + <div class="col"> + <div class="lead"> + Dataset <span class="font-weight-bold">{{ (dataset | async).label }}</span> + selected with <span class="font-weight-bold">{{ dataLength | async }}</span> objects found. + </div> + <div class="lead mt-4"> + <app-download-result + [instance]="instance | async" + [dataset]="dataset | async" + [attributeList]="attributeList | async" + [criteriaList]="criteriaList | async" + [outputList]="outputList | async" + [coneSearch]="coneSearch | async" + [sampRegistered]="sampRegistered | async" + [archiveIsCreating]="archiveIsCreating | async" + (sampRegister)="sampRegister()" + (sampUnregister)="sampUnregister()" + (downloadFile)="downloadFile($event)" + (broadcastVotable)="broadcastVotable($event)" + (startTaskCreateArchive)="startTaskCreateArchive($event)"> + </app-download-result> + </div> </div> - <div class="lead mt-3"> - Download results: - <a class="btn btn-primary" title="Download results in JSON format"> - <span class="fas fa-file"></span> JSON - </a> - - <a class="btn btn-primary" title="Download results in CSV format"> - <span class="fas fa-file-csv"></span> CSV - </a> - - <a class="btn btn-primary" title="Download results in ASCII format"> - <span class="fas fa-file"></span> ASCII - </a> - - <a class="btn btn-primary" title="Download results in VO format"> - <span class="fas fa-file"></span> VOtable - </a> + <div class="col-md-6" *ngIf="coneSearch | async"> + <app-image-list-result + [dataset]="dataset | async" + [attributeList]="attributeList | async" + [coneSearch]="coneSearch | async" + [data]="data | async" + [dataIsLoading]="dataIsLoading | async" + [dataIsLoaded]="dataIsLoaded | async" + [imageList]="imageList | async" + [imageListIsLoading]="imageListIsLoading | async" + [imageListIsLoaded]="imageListIsLoaded | async" + [sampRegistered]="sampRegistered | async" + (downloadFile)="downloadFile($event)"> + </app-image-list-result> + </div> + </div> + <div class="row"> + <div class="col-2"> + <app-datatable-actions + [dataset]="dataset | async" + [criteriaList]="criteriaList | async" + [outputList]="outputList | async" + [coneSearch]="coneSearch | async" + [attributeList]="attributeList | async" + [selectedData]="selectedData | async" + [sampRegistered]="sampRegistered | async" + [archiveIsCreating]="archiveIsCreating | async" + (downloadFile)="downloadFile($event)" + (broadcastVotable)="broadcastVotable($event)" + (startTaskCreateArchive)="startTaskCreateArchive($event)"> + </app-datatable-actions> + </div> + <div class="col align-self-center"> + <app-url-display + [dataset]="dataset | async" + [criteriaList]="criteriaList | async" + [outputList]="outputList | async" + [coneSearch]="coneSearch | async"> + </app-url-display> </div> </div> - - <app-datatable-actions - [selectedData]="selectedData | async" - [datasetSelected]="datasetSelected | async" - [datasetList]="datasetList | async" - [sampRegistered]="sampRegistered | async" - (broadcast)="broadcastVotable($event)" - (startTaskCreateResult)="startTaskCreateResult($event)" - (startTaskCreateArchive)="startTaskCreateArchive($event)"> - </app-datatable-actions> - <app-datatable [dataset]="dataset | async" [instance]="instance | async" @@ -70,80 +100,6 @@ (deleteSelectedData)="deleteSearchData($event)" (downloadFile)="downloadFile($event)"> </app-datatable> - - <!-- <div *ngIf="(instance | async).samp_enabled" class="jumbotron mb-4 py-4"> - <div class="lead"> - <ng-container *ngIf="!(sampRegistered | async)"> - You are not connected to a SAMP-hub - <button (click)="sampRegister()" class="btn btn-outline-primary">Try to register</button> - </ng-container> - <ng-container *ngIf="(sampRegistered | async)"> - You are connected to a SAMP-hub - <button (click)="sampUnregister()" class="btn btn-outline-primary">Unregister</button> - </ng-container> - </div> - </div> - <app-download-file-tab - *ngIf="(downloadedFiles | async).length > 0" - [downloadedFiles]="downloadedFiles | async"> - </app-download-file-tab> - <app-download - [datasetSelected]="datasetSelected | async" - [datasetList]="datasetList | async" - [attributeList]="attributeList | async" - [criteriaList]="criteriaList | async" - [outputList]="outputList | async" - [coneSearch]="coneSearch | async" - [dataLength]="dataLength | async" - [sampRegistered]="sampRegistered | async" - (broadcast)="broadcastVotable($event)" - (startTaskCreateResult)="startTaskCreateResult($event)" - (startTaskCreateArchive)="startTaskCreateArchive($event)"> - </app-download> - <app-reminder - [datasetSelected]="datasetSelected | async" - [datasetList]="datasetList | async" - [attributeList]="attributeList | async | sortByOutputDisplay" - [criteriaFamilyList]="criteriaFamilyList | async" - [outputFamilyList]="outputFamilyList | async" - [outputCategoryList]="outputCategoryList | async" - [criteriaList]="criteriaList | async" - [coneSearch]="coneSearch | async" - [outputList]="outputList | async"> - </app-reminder> - <app-url-display - [datasetSelected]="datasetSelected | async" - [datasetList]="datasetList | async" - [criteriaList]="criteriaList | async" - [outputList]="outputList | async" - [coneSearch]="coneSearch | async"> - </app-url-display> - <app-datatable-tab - [datasetSelected]="datasetSelected | async" - [instance]="instance | async" - [datasetList]="datasetList | async" - [attributeList]="attributeList | async | sortByOutputDisplay" - [criteriaList]="criteriaList | async" - [outputList]="outputList | async" - [coneSearch]="coneSearch | async" - [queryParams]="queryParams | async" - [dataLength]="dataLength | async" - [sampRegistered]="sampRegistered | async" - [data]="data | async" - [dataIsLoading]="dataIsLoading | async" - [dataIsLoaded]="dataIsLoaded | async" - [selectedData]="selectedData | async" - [imageList]="imageList | async" - [imageListIsLoading]="imageListIsLoading | async" - [imageListIsLoaded]="imageListIsLoaded | async" - (retrieveData)="retrieveData($event)" - (addSelectedData)="addSearchData($event)" - (deleteSelectedData)="deleteSearchData($event)" - (broadcast)="broadcastVotable($event)" - (startTaskCreateResult)="startTaskCreateResult($event)" - (startTaskCreateArchive)="startTaskCreateArchive($event)" - (downloadFile)="downloadFile($event)"> - </app-datatable-tab> --> </ng-container> </div> </div> diff --git a/client/src/app/instance/search/containers/result.component.ts b/client/src/app/instance/search/containers/result.component.ts index 979bd1dca280e87454f1b182e83c1c3ad686c640..66d38e4ccb9c510d40fa71253455b40d393c2ef1 100644 --- a/client/src/app/instance/search/containers/result.component.ts +++ b/client/src/app/instance/search/containers/result.component.ts @@ -13,15 +13,15 @@ import { Store } from '@ngrx/store'; import { Observable, Subscription } from 'rxjs'; import { AbstractSearchComponent } from './abstract-search.component'; -import { Pagination, DownloadFile } from '../../store/models'; +import { Pagination } from '../../store/models'; import { Instance, Dataset, Image } from 'src/app/metamodel/models'; import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; import * as searchActions from '../../store/actions/search.actions'; import * as searchSelector from '../../store/selectors/search.selector'; import * as sampActions from 'src/app/samp/samp.actions'; import * as sampSelector from 'src/app/samp/samp.selector'; -import * as downloadFileActions from '../../store/actions/download-file.actions'; -import * as downloadFileSelector from '../../store/selectors/download-file.selector'; +import * as archiveActions from '../../store/actions/archive.actions'; +import * as archiveSelector from '../../store/selectors/archive.selector'; import * as imageActions from 'src/app/metamodel/actions/image.actions'; import * as imageSelector from 'src/app/metamodel/selectors/image.selector'; import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector'; @@ -48,10 +48,10 @@ export class ResultComponent extends AbstractSearchComponent { public dataIsLoaded: Observable<boolean>; public selectedData: Observable<any>; public sampRegistered: Observable<boolean>; - public downloadedFiles: Observable<DownloadFile[]>; public imageList: Observable<Image[]>; public imageListIsLoading: Observable<boolean>; public imageListIsLoaded: Observable<boolean>; + public archiveIsCreating: Observable<boolean>; public pristineSubscription: Subscription; constructor(protected store: Store<{ }>) { @@ -66,10 +66,10 @@ export class ResultComponent extends AbstractSearchComponent { this.dataIsLoaded = this.store.select(searchSelector.selectDataIsLoaded); this.selectedData = this.store.select(searchSelector.selectSelectedData); this.sampRegistered = this.store.select(sampSelector.selectRegistered); - this.downloadedFiles = this.store.select(downloadFileSelector.selectDownloadedFiles); this.imageList = this.store.select(imageSelector.selectAllImages); this.imageListIsLoading = this.store.select(imageSelector.selectImageListIsLoading); this.imageListIsLoaded = this.store.select(imageSelector.selectImageListIsLoaded); + this.archiveIsCreating = this.store.select(archiveSelector.selectArchiveIsCreating); } ngOnInit(): void { @@ -136,30 +136,16 @@ export class ResultComponent extends AbstractSearchComponent { this.store.dispatch(searchActions.deleteSelectedData({ id })); } - /** - * Dispatches action to starts downloading file. - * - * @param {url: string, fileId: string, datasetName: string, filename: string} download - Info about file to download - */ - downloadFile(download: {url: string, fileId: string, datasetName: string, filename: string}): void { - this.store.dispatch(downloadFileActions.downloadFile(download)); - } - - /** - * Dispatches action to starts task create result and download - * - * @param string format - Info about result format - */ - startTaskCreateResult(event: { format: string, selectedData: boolean, broadcastVo: boolean }) { - this.store.dispatch(downloadFileActions.startTaskCreateResult(event)); + downloadFile(download: {url: string, filename: string}): void { + this.store.dispatch(searchActions.downloadFile(download)); } /** * Dispatches action to starts task create archive and download * */ - startTaskCreateArchive(event: { selectedData: boolean }) { - this.store.dispatch(downloadFileActions.startTaskCreateArchive(event)); + startTaskCreateArchive(query: string) { + this.store.dispatch(archiveActions.startTaskCreateArchive({ query })); } /** @@ -167,6 +153,7 @@ export class ResultComponent extends AbstractSearchComponent { */ ngOnDestroy(): void { this.store.dispatch(searchActions.destroyResults()); + this.store.dispatch(archiveActions.resetArchive()); if (this.pristineSubscription) this.pristineSubscription.unsubscribe(); super.ngOnDestroy(); } diff --git a/client/src/app/instance/store/actions/archive.actions.ts b/client/src/app/instance/store/actions/archive.actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..66275bcea8f51501166a9725e4c12b89f7e923b4 --- /dev/null +++ b/client/src/app/instance/store/actions/archive.actions.ts @@ -0,0 +1,18 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createAction, props } from '@ngrx/store'; + +export const startTaskCreateArchive = createAction('[Archive] Start Task Create Archive', props<{ query: string }>()); +export const startTaskCreateArchiveSuccess = createAction('[Archive] Start Task Create Archive Success', props<{ fileId: string, filename: string, datasetName: string }>()); +export const startTaskCreateArchiveFail = createAction('[Archive] Start Task Create Archive Fail'); +export const isArchiveAvailable = createAction('[Archive] Is Archive Available', props<{ fileId: string, filename: string, datasetName: string }>()); +export const isArchiveAvailableSuccess = createAction('[Archive] Is Archive Available Success', props<{ url: string, filename: string }>()); +export const isArchiveAvailableFail = createAction('[Archive] Is Archive Available Fail'); +export const resetArchive = createAction('[Archive] Reset Archive'); \ No newline at end of file diff --git a/client/src/app/instance/store/actions/download-file.actions.ts b/client/src/app/instance/store/actions/download-file.actions.ts deleted file mode 100644 index 60fefb6f1cadd470066f5b081b2afcf553d23a93..0000000000000000000000000000000000000000 --- a/client/src/app/instance/store/actions/download-file.actions.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { createAction, props } from '@ngrx/store'; - -export const startTaskCreateResult = createAction('[File] Start Task Create Result', props<{ format: string, selectedData: boolean, broadcastVo: boolean }>()); -export const startTaskCreateResultSuccess = createAction('[File] Start Task Create Result Success', props<{ fileId: string, datasetName: string, filename: string, broadcastVo: boolean }>()); -export const startTaskCreateResultFail = createAction('[File] Start Task Create Result Fail'); -export const isResultAvailable = createAction('[File] Is Result Available', props<{ fileId: string, datasetName: string, filename: string, broadcastVo: boolean }>()); -export const isResultAvailableFail = createAction('[File] Is Result Available Fail'); - -export const startTaskCreateArchive = createAction('[File] Start Task Create Archive', props<{ selectedData: boolean }>()); -export const startTaskCreateArchiveSuccess = createAction('[File] Start Task Create Archive Success', props<{ fileId: string, datasetName: string, filename: string }>()); -export const startTaskCreateArchiveFail = createAction('[File] Start Task Create Archive Fail'); -export const isArchiveAvailable = createAction('[File] Is Archive Available', props<{ fileId: string, datasetName: string, filename: string }>()); -export const isArchiveAvailableFail = createAction('[File] Is Archive Available Fail'); - -export const downloadFile = createAction('[File] Download File', props<{ url: string, fileId: string, datasetName: string, filename: string }>()); - -export const startsDownloadingFile = createAction('[File] Starts Downloading File', props<{ url: string, filename: string, fileId: string }>()); -export const updateDownloadProgress = createAction('[File] Update Download Progress', props<{ progress: number, fileId: string }>()); -export const fileDownloaded = createAction('[File] File Downloaded', props<{ fileId: string }>()); diff --git a/client/src/app/instance/store/actions/search.actions.ts b/client/src/app/instance/store/actions/search.actions.ts index b3fe3a958d3326a647afe189347b4ff8c645040b..86333f653f87a2d19ab33f8ed3e092e20654105c 100644 --- a/client/src/app/instance/store/actions/search.actions.ts +++ b/client/src/app/instance/store/actions/search.actions.ts @@ -34,3 +34,4 @@ export const retrieveDataSuccess = createAction('[Search] Retrieve Data Success' export const retrieveDataFail = createAction('[Search] Retrieve Data Fail'); export const addSelectedData = createAction('[Search] Add Selected Data', props<{ id: number | string }>()); export const deleteSelectedData = createAction('[Search] Delete Selected Data', props<{ id: number | string }>()); +export const downloadFile = createAction('[Search] Download File', props<{ url: string, filename: string }>()); diff --git a/client/src/app/instance/store/effects/archive.effects.ts b/client/src/app/instance/store/effects/archive.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ce9973e4d576eff539f20f95567d4a6e3e53076 --- /dev/null +++ b/client/src/app/instance/store/effects/archive.effects.ts @@ -0,0 +1,123 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Injectable } from '@angular/core'; + +import { Actions, createEffect, ofType, concatLatestFrom } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { of, timer, mapTo, takeUntil, Subject } from 'rxjs'; +import { map, tap, mergeMap, switchMap, catchError } from 'rxjs/operators'; +import { ToastrService } from 'ngx-toastr'; + +import { ArchiveService } from '../services/archive.service'; +import * as archiveActions from '../actions/archive.actions'; +import * as searchSelector from '../selectors/search.selector'; +import * as searchActions from '../actions/search.actions'; +import { AppConfigService } from 'src/app/app-config.service'; +import { getHost } from 'src/app/shared/utils'; + +/** + * @class + * @classdesc File effects. + */ +@Injectable() +export class ArchiveEffects { + startTaskCreateArchive$ = createEffect(() => + this.actions$.pipe( + ofType(archiveActions.startTaskCreateArchive), + concatLatestFrom(() => this.store.select(searchSelector.selectCurrentDataset)), + mergeMap(([action, datasetName]) => { + return this.archiveService.startTaskCreateArchive(action.query) + .pipe( + map((response) => archiveActions.startTaskCreateArchiveSuccess({ + fileId: response.archive_id, + filename: response.archive_name, + datasetName + })), + catchError(() => of(archiveActions.startTaskCreateArchiveFail())) + ) + }) + ) + ); + + startTaskCreateArchiveSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(archiveActions.startTaskCreateArchiveSuccess), + tap(action => this.kill$ = new Subject()), + switchMap(action => timer(0, 1000) + .pipe( + mapTo(archiveActions.isArchiveAvailable(action)), + takeUntil(this.kill$) + ) + ) + ) + ); + + startTaskCreateArchiveFail$ = createEffect(() => + this.actions$.pipe( + ofType(archiveActions.startTaskCreateArchiveFail), + tap(() => this.toastr.error('The creation of the archive file failed', 'Start async task failed')) + ), { dispatch: false} + ); + + isArchiveAvailable$ = createEffect(() => + this.actions$.pipe( + ofType(archiveActions.isArchiveAvailable), + switchMap(action => this.archiveService.isArchiveAvailable(action.fileId) + .pipe( + map(result => { + if (result.archive_is_available) { + this.kill$.next({}); + this.kill$.unsubscribe(); + + return archiveActions.isArchiveAvailableSuccess({ + url: `${getHost(this.config.apiUrl)}/download-archive/${action.datasetName}/${action.fileId}`, + filename: action.filename + }); + } else { + return { type: '[No Action] Is Archive Available' }; + } + }), + catchError(() => of(archiveActions.isArchiveAvailableFail())) + ) + ) + ) + ); + + isArchiveAvailableSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(archiveActions.isArchiveAvailableSuccess), + map(action => searchActions.downloadFile(action)) + ) + ); + + isArchiveAvailableFail$ = createEffect(() => + this.actions$.pipe( + ofType(archiveActions.isArchiveAvailableFail), + tap(() => this.toastr.error('The creation of the archive has encountered a problem', 'Archive result download failed')) + ), { dispatch: false} + ); + + resetArchive$ = createEffect(() => + this.actions$.pipe( + ofType(archiveActions.resetArchive), + tap(() => this.kill$.unsubscribe()) + ), { dispatch: false} + ); + + private kill$: Subject<{}>; + + constructor( + private actions$: Actions, + private archiveService: ArchiveService, + private store: Store<{ }>, + private toastr: ToastrService, + private config: AppConfigService + ) {} +} diff --git a/client/src/app/instance/store/effects/download-file.effects.ts b/client/src/app/instance/store/effects/download-file.effects.ts deleted file mode 100644 index 437e0eb6e040315479695f9743e0cd271290385e..0000000000000000000000000000000000000000 --- a/client/src/app/instance/store/effects/download-file.effects.ts +++ /dev/null @@ -1,272 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Injectable } from '@angular/core'; -import { HttpEventType } from '@angular/common/http'; - -import { Actions, createEffect, ofType, concatLatestFrom } from '@ngrx/effects'; -import { Store } from '@ngrx/store'; -import { of, timer, mapTo, takeUntil, Subject } from 'rxjs'; -import { map, tap, mergeMap, switchMap, catchError } from 'rxjs/operators'; -import { ToastrService } from 'ngx-toastr'; - -import { AppConfigService } from 'src/app/app-config.service'; -import { DownloadFileService } from '../services/download-file.service'; -import * as downloadFileActions from '../actions/download-file.actions'; -import * as sampActions from 'src/app/samp/samp.actions'; -import * as searchSelector from '../selectors/search.selector'; -import * as attributeSelector from 'src/app/metamodel/selectors/attribute.selector'; -import * as coneSearchSelector from '../selectors/cone-search.selector'; - -/** - * @class - * @classdesc File effects. - */ -@Injectable() -export class DownloadFileEffects { - startTaskCreateResult$ = createEffect(() => - this.actions$.pipe( - ofType(downloadFileActions.startTaskCreateResult), - concatLatestFrom(() => [ - this.store.select(searchSelector.selectCurrentDataset), - this.store.select(attributeSelector.selectAllAttributes), - this.store.select(searchSelector.selectCriteriaListByRoute), - this.store.select(coneSearchSelector.selectConeSearchByRoute), - this.store.select(searchSelector.selectOutputListByRoute), - this.store.select(searchSelector.selectSelectedData) - ]), - mergeMap(([action, currentDataset, attributeList, criteriaList, coneSearch, outputList, selectedData]) => { - const attributeId = attributeList.find(a => a.primary_key); - let query: string = `${currentDataset}?a=${outputList}`; - if (criteriaList) { - query += `&c=${criteriaList}`; - if (selectedData && action.selectedData) { - query += `;${attributeId.id}::in::${selectedData.join('|')}`; - } - } else if (selectedData && action.selectedData) { - query += `&c=${attributeId.id}::in::${selectedData.join('|')}`; - } - if (coneSearch) { - query += `&cs=${coneSearch}`; - } - query += `&f=${action.format}`; - - return this.downloadFileService.startTaskCreateResult(query) - .pipe( - map((response) => downloadFileActions.startTaskCreateResultSuccess({ - fileId: response.file_id, - filename: response.file_name, - datasetName: currentDataset, - broadcastVo: action.broadcastVo - })), - catchError(() => of(downloadFileActions.startTaskCreateResultFail())) - ) - }) - ) - ); - - startTaskCreateResultSuccess$ = createEffect(() => - this.actions$.pipe( - ofType(downloadFileActions.startTaskCreateResultSuccess), - tap(action => this.kill$[action.fileId] = new Subject()), - switchMap(action => timer(0, 1000) - .pipe( - mapTo(downloadFileActions.isResultAvailable(action)), - takeUntil(this.kill$[action.fileId]) - ) - ) - ) - ); - - startTaskCreateResultFail$ = createEffect(() => - this.actions$.pipe( - ofType(downloadFileActions.startTaskCreateResultFail), - tap(() => this.toastr.error('The creation of the result file failed', 'Start async task failed')) - ), { dispatch: false} - ); - - isResultAvailable$ = createEffect(() => - this.actions$.pipe( - ofType(downloadFileActions.isResultAvailable), - switchMap(action => this.downloadFileService.isResultAvailable(action.fileId) - .pipe( - switchMap(result => { - if (result.file_is_available) { - this.kill$[action.fileId].next({}); - this.kill$[action.fileId].unsubscribe(); - if (action.broadcastVo) { - return [ - sampActions.broadcastVotable({ - url: `${this.config.apiUrl}/download-result/${action.datasetName}/${action.fileId}` - }), - downloadFileActions.fileDownloaded({ fileId: action.fileId }) - ]; - } else { - return [ - downloadFileActions.startsDownloadingFile({ - fileId: action.fileId, - filename: action.filename, - url: `${this.config.apiUrl}/download-result/${action.datasetName}/${action.fileId}` - }) - ]; - } - } else { - return [{ type: '[No Action] Is Result Available' }]; - } - }), - catchError(() => of(downloadFileActions.isResultAvailableFail())) - ) - ) - ) - ); - - isResultAvailableFail$ = createEffect(() => - this.actions$.pipe( - ofType(downloadFileActions.isResultAvailableFail), - tap(() => this.toastr.error('The creation of the file has encountered a problem', 'File result download failed')) - ), { dispatch: false} - ); - - startTaskCreateArchive$ = createEffect(() => - this.actions$.pipe( - ofType(downloadFileActions.startTaskCreateArchive), - concatLatestFrom(() => [ - this.store.select(searchSelector.selectCurrentDataset), - this.store.select(attributeSelector.selectAllAttributes), - this.store.select(searchSelector.selectCriteriaListByRoute), - this.store.select(coneSearchSelector.selectConeSearchByRoute), - this.store.select(searchSelector.selectOutputListByRoute), - this.store.select(searchSelector.selectSelectedData) - ]), - mergeMap(([action, currentDataset, attributeList, criteriaList, coneSearch, outputList, selectedData]) => { - const attributeId = attributeList.find(a => a.primary_key); - let query: string = `${currentDataset}?a=${outputList}`; - if (criteriaList) { - query += `&c=${criteriaList}`; - if (selectedData && action.selectedData) { - query += `;${attributeId.id}::in::${selectedData.join('|')}`; - } - } else if (selectedData && action.selectedData) { - query += `&c=${attributeId.id}::in::${selectedData.join('|')}`; - } - if (coneSearch) { - query += `&cs=${coneSearch}`; - } - - return this.downloadFileService.startTaskCreateArchive(query) - .pipe( - map((response) => downloadFileActions.startTaskCreateArchiveSuccess({ - fileId: response.archive_id, - filename: response.archive_name, - datasetName: currentDataset - })), - catchError(() => of(downloadFileActions.startTaskCreateArchiveFail())) - ) - }) - ) - ); - - startTaskCreateArchiveSuccess$ = createEffect(() => - this.actions$.pipe( - ofType(downloadFileActions.startTaskCreateArchiveSuccess), - tap(action => this.kill$[action.fileId] = new Subject()), - switchMap(action => timer(0, 1000) - .pipe( - mapTo(downloadFileActions.isArchiveAvailable(action)), - takeUntil(this.kill$[action.fileId]) - ) - ) - ) - ); - - startTaskCreateArchiveFail$ = createEffect(() => - this.actions$.pipe( - ofType(downloadFileActions.startTaskCreateArchiveFail), - tap(() => this.toastr.error('The creation of the archive file failed', 'Start async task failed')) - ), { dispatch: false} - ); - - isArchiveAvailable$ = createEffect(() => - this.actions$.pipe( - ofType(downloadFileActions.isArchiveAvailable), - switchMap(action => this.downloadFileService.isArchiveAvailable(action.fileId) - .pipe( - map(result => { - if (result.archive_is_available) { - this.kill$[action.fileId].next({}); - this.kill$[action.fileId].unsubscribe(); - return downloadFileActions.startsDownloadingFile({ - fileId: action.fileId, - filename: action.filename, - url: `${this.config.apiUrl}/download-archive/${action.datasetName}/${action.fileId}` - }); - } else { - return { type: '[No Action] Is Archive Available' }; - } - }), - catchError(() => of(downloadFileActions.isArchiveAvailableFail())) - ) - ) - ) - ); - - isArchiveAvailableFail$ = createEffect(() => - this.actions$.pipe( - ofType(downloadFileActions.isArchiveAvailableFail), - tap(() => this.toastr.error('The creation of the archive has encountered a problem', 'Archive result download failed')) - ), { dispatch: false} - ); - - downloadFile$ = createEffect(() => - this.actions$.pipe( - ofType(downloadFileActions.downloadFile), - map(action => downloadFileActions.startsDownloadingFile({ - url: action.url, - fileId: action.fileId, - filename: action.filename - })) - ) - ); - - /** - * Calls actions to retrieve object. - */ - startsDownloadingFile$ = createEffect(() => - this.actions$.pipe( - ofType(downloadFileActions.startsDownloadingFile), - mergeMap((action) => this.downloadFileService.startsDownloadingFile(action.url) - .pipe( - map(event => { - if (event.type === HttpEventType.DownloadProgress) { - this.store.dispatch(downloadFileActions.updateDownloadProgress({ - progress: Math.round((100 * event.loaded) / event.total), - fileId: action.fileId - })); - } - - if (event.type === HttpEventType.Response) { - this.store.dispatch(downloadFileActions.fileDownloaded({ fileId: action.fileId })); - this.downloadFileService.saveDownloadedFile(event.body as Blob, action.filename); - } - }) - ) - ) - ), { dispatch: false } - ); - - private kill$ = []; - - constructor( - private actions$: Actions, - private downloadFileService: DownloadFileService, - private store: Store<{ }>, - private toastr: ToastrService, - private config: AppConfigService - ) {} -} diff --git a/client/src/app/instance/store/effects/index.ts b/client/src/app/instance/store/effects/index.ts index ddaa3d86df6a14dc0dababa0aa9fafafd4dad401..54904d02d268a79b5399d5e74f70dc83bdb5c542 100644 --- a/client/src/app/instance/store/effects/index.ts +++ b/client/src/app/instance/store/effects/index.ts @@ -3,7 +3,7 @@ import { SearchMultipleEffects } from './search-multiple.effects'; import { ConeSearchEffects } from './cone-search.effects'; import { DetailEffects } from './detail.effects'; import { SvomJsonKwEffects } from './svom-json-kw.effects'; -import { DownloadFileEffects } from './download-file.effects'; +import { ArchiveEffects } from './archive.effects'; export const instanceEffects = [ SearchEffects, @@ -11,5 +11,5 @@ export const instanceEffects = [ ConeSearchEffects, DetailEffects, SvomJsonKwEffects, - DownloadFileEffects + ArchiveEffects ]; diff --git a/client/src/app/instance/store/effects/search.effects.ts b/client/src/app/instance/store/effects/search.effects.ts index 157f1b275b37819acf76c9e0495d81e2b19ba250..9516c082b3af6a4a318ad23cd7c6f458bf8bb16e 100644 --- a/client/src/app/instance/store/effects/search.effects.ts +++ b/client/src/app/instance/store/effects/search.effects.ts @@ -14,6 +14,8 @@ import { Store, Action } from '@ngrx/store'; import { of } from 'rxjs'; import { map, tap, mergeMap, catchError } from 'rxjs/operators'; import { ToastrService } from 'ngx-toastr'; +import { KeycloakService } from 'keycloak-angular'; +import FileSaver from 'file-saver'; import { ConeSearch, criterionToString, stringToCriterion } from '../models'; import { SearchService } from '../services/search.service'; @@ -27,6 +29,7 @@ import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector'; import * as searchSelector from '../selectors/search.selector'; import * as coneSearchActions from '../actions/cone-search.actions'; import * as coneSearchSelector from '../selectors/cone-search.selector'; +import * as authSelector from 'src/app/auth/auth.selector'; /** * @class @@ -249,10 +252,31 @@ export class SearchEffects { ), { dispatch: false} ); + downloadFile$ = createEffect(() => + this.actions$.pipe( + ofType(searchActions.downloadFile), + concatLatestFrom(() => this.store.select(authSelector.selectIsAuthenticated)), + tap(([action, isAuthenticated]) => { + if (isAuthenticated) { + this.keycloak.getToken().then(token => { + let separator = '?'; + if (action.url.indexOf('?') > -1) { + separator = '&'; + } + FileSaver.saveAs(`${action.url}${separator}token=${token}`, action.filename); + }); + } else { + FileSaver.saveAs(action.url, action.filename); + } + }) + ), { dispatch: false} + ); + constructor( private actions$: Actions, private searchService: SearchService, private store: Store<{ }>, - private toastr: ToastrService + private toastr: ToastrService, + private keycloak: KeycloakService, ) {} } diff --git a/client/src/app/instance/store/models/download-file.model.ts b/client/src/app/instance/store/models/download-file.model.ts deleted file mode 100644 index 0d21ff574b88548eb8067f65b2b617377bba56f9..0000000000000000000000000000000000000000 --- a/client/src/app/instance/store/models/download-file.model.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -export interface DownloadFile { - id: string; - datasetName: string; - fileName: string; - state: 'PENDING' | 'IN_PROGRESS' | 'DONE'; - progress: number; -} diff --git a/client/src/app/instance/store/models/index.ts b/client/src/app/instance/store/models/index.ts index 3b2241bcfbdd3629726f8766f5220e0b74b9ccf1..67be1059cf6ae8f9f0530f91f275da1cf1252ae6 100644 --- a/client/src/app/instance/store/models/index.ts +++ b/client/src/app/instance/store/models/index.ts @@ -8,4 +8,3 @@ export * from './resolver.model'; export * from './search-multiple-dataset-length'; export * from './search-multiple-dataset-data'; export * from './svom-keyword.model'; -export * from './download-file.model'; diff --git a/client/src/app/instance/store/reducers/archive.reducer.ts b/client/src/app/instance/store/reducers/archive.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..05400b0dd047c325445dfdf700d13c62d00c11ad --- /dev/null +++ b/client/src/app/instance/store/reducers/archive.reducer.ts @@ -0,0 +1,42 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createReducer, on } from '@ngrx/store'; + +import * as archiveActions from '../actions/archive.actions'; + +/** + * Interface for file state. + * + * @interface State + */ +export interface State { + archiveIsCreating: boolean; +} + +export const initialState: State = { + archiveIsCreating: false +}; + +export const archiveReducer = createReducer( + initialState, + on(archiveActions.startTaskCreateArchiveSuccess, state => ({ + ...state, + archiveIsCreating: true + })), + on(archiveActions.isArchiveAvailableSuccess, state => ({ + ...state, + archiveIsCreating: false + })), + on(archiveActions.resetArchive, () => ({ + ...initialState + })) +); + +export const selectArchiveIsCreating = (state: State) => state.archiveIsCreating; diff --git a/client/src/app/instance/store/reducers/download-file.reducer.ts b/client/src/app/instance/store/reducers/download-file.reducer.ts deleted file mode 100644 index 372e24a9c543725a6aa317d31a5ac80f449892d0..0000000000000000000000000000000000000000 --- a/client/src/app/instance/store/reducers/download-file.reducer.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { createReducer, on } from '@ngrx/store'; - -import { DownloadFile } from '../models'; -import * as downloadFileActions from '../actions/download-file.actions'; - -/** - * Interface for file state. - * - * @interface State - */ -export interface State { - downloadedFiles: DownloadFile[] -} - -export const initialState: State = { - downloadedFiles: [] -}; - -export const fileReducer = createReducer( - initialState, - on(downloadFileActions.startTaskCreateResultSuccess, (state, {fileId, datasetName, filename }) => ({ - ...state, - downloadedFiles: [...state.downloadedFiles, { - id: fileId, - datasetName, - fileName: filename, - state: 'PENDING', - progress: 0 - }] - })), - on(downloadFileActions.startTaskCreateArchiveSuccess, (state, {fileId, datasetName, filename }) => ({ - ...state, - downloadedFiles: [...state.downloadedFiles, { - id: fileId, - datasetName, - fileName: filename, - state: 'PENDING', - progress: 0 - }] - })), - on(downloadFileActions.downloadFile, (state, {fileId, datasetName, filename }) => ({ - ...state, - downloadedFiles: [...state.downloadedFiles, { - id: fileId, - datasetName, - fileName: filename, - state: 'PENDING', - progress: 0 - }] - })), - on(downloadFileActions.updateDownloadProgress, (state, { progress, fileId }) => ({ - ...state, - downloadedFiles: [ - ...state.downloadedFiles.filter(f => f.id !== fileId), - { - ...state.downloadedFiles.find(f => f.id === fileId), - progress: progress, - state: 'IN_PROGRESS' - } - ] - })), - on(downloadFileActions.fileDownloaded, (state, { fileId }) => ({ - ...state, - downloadedFiles: [ - ...state.downloadedFiles.filter(f => f.id !== fileId), - { - ...state.downloadedFiles.find(f => f.id === fileId), - state: 'DONE', - progress: 100 - } - ] - })) -); - -export const selectDownloadedFiles = (state: State) => state.downloadedFiles; \ No newline at end of file diff --git a/client/src/app/instance/store/selectors/download-file.selector.ts b/client/src/app/instance/store/selectors/archive.selector.ts similarity index 55% rename from client/src/app/instance/store/selectors/download-file.selector.ts rename to client/src/app/instance/store/selectors/archive.selector.ts index b955521d86de25bbf5c6eef631a4b77e4f2bd982..fbcf2238778109a69a81f211c0297ddc14c5dcd5 100644 --- a/client/src/app/instance/store/selectors/download-file.selector.ts +++ b/client/src/app/instance/store/selectors/archive.selector.ts @@ -10,14 +10,14 @@ import { createSelector } from '@ngrx/store'; import * as reducer from '../../instance.reducer'; -import * as fromDownloadFile from '../reducers/download-file.reducer'; +import * as fromArchive from '../reducers/archive.reducer'; -export const selectDownloadFileState = createSelector( +export const selectArchiveState = createSelector( reducer.getInstanceState, - (state: reducer.State) => state.downloadFile + (state: reducer.State) => state.archive ); -export const selectDownloadedFiles = createSelector( - selectDownloadFileState, - fromDownloadFile.selectDownloadedFiles +export const selectArchiveIsCreating = createSelector( + selectArchiveState, + fromArchive.selectArchiveIsCreating ); diff --git a/client/src/app/instance/store/services/archive.service.ts b/client/src/app/instance/store/services/archive.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc7c948477f5ed0b1c19f6aef0683f21f6cf4676 --- /dev/null +++ b/client/src/app/instance/store/services/archive.service.ts @@ -0,0 +1,26 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { AppConfigService } from 'src/app/app-config.service'; + +@Injectable({providedIn: 'root'}) +export class ArchiveService { + constructor(private http: HttpClient, private config: AppConfigService) { } + + startTaskCreateArchive(query: string) { + return this.http.get<{"archive_name": string, "archive_id": string}>(`${this.config.apiUrl}/start-task-create-archive/${query}`); + } + + isArchiveAvailable(id: string) { + return this.http.get<{"archive_is_available": boolean}>(`${this.config.apiUrl}/is-archive-available/${id}`); + } +} \ No newline at end of file diff --git a/client/src/app/instance/store/services/download-file.service.ts b/client/src/app/instance/store/services/download-file.service.ts deleted file mode 100644 index 25c26182e9f69d10ee75b025fde4cbed781d3a91..0000000000000000000000000000000000000000 --- a/client/src/app/instance/store/services/download-file.service.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Injectable } from '@angular/core'; -import { HttpClient, HttpRequest } from '@angular/common/http'; - -import { AppConfigService } from 'src/app/app-config.service'; - -@Injectable({providedIn: 'root'}) -export class DownloadFileService { - constructor(private http: HttpClient, private config: AppConfigService) { } - - /** - * Returns file name and file ID - * - * @param {string} query - The query. - * - * @return Observable<{"file_name": string, "file_id": string}> - */ - startTaskCreateResult(query: string) { - return this.http.get<{"file_name": string, "file_id": string}>(`${this.config.apiUrl}/start-task-create-result/${query}`); - } - - isResultAvailable(id: string) { - return this.http.get<{"file_is_available": boolean}>(`${this.config.apiUrl}/is-result-available/${id}`); - } - - startTaskCreateArchive(query: string) { - return this.http.get<{"archive_name": string, "archive_id": string}>(`${this.config.apiUrl}/start-task-create-archive/${query}`); - } - - isArchiveAvailable(id: string) { - return this.http.get<{"archive_is_available": boolean}>(`${this.config.apiUrl}/is-archive-available/${id}`); - } - - startsDownloadingFile(url: string) { - const request = new HttpRequest( - "GET", - url, - {}, - { reportProgress: true, responseType: 'blob' } - ); - - return this.http.request(request); - } - - saveDownloadedFile(body: Blob, filename: string) { - let downloadLink = document.createElement('a'); - const url = window.URL.createObjectURL(body); - downloadLink.href = url; - downloadLink.setAttribute('download', filename); - downloadLink.click(); - window.URL.revokeObjectURL(url); - } -} \ No newline at end of file diff --git a/client/src/app/instance/store/services/index.ts b/client/src/app/instance/store/services/index.ts index 4f4c2847d6d9b98de06b4daef5509ffb8e82a3ba..6ea85f7ae67883198ce83ba02b1e902d97d7e002 100644 --- a/client/src/app/instance/store/services/index.ts +++ b/client/src/app/instance/store/services/index.ts @@ -2,12 +2,12 @@ import { SearchService } from './search.service'; import { ConeSearchService } from './cone-search.service'; import { DetailService } from './detail.service'; import { SvomJsonKwService } from './svom-json-kw.service'; -import { DownloadFileService } from './download-file.service'; +import { ArchiveService } from './archive.service'; export const instanceServices = [ SearchService, ConeSearchService, DetailService, SvomJsonKwService, - DownloadFileService + ArchiveService ]; diff --git a/client/src/app/samp/samp.actions.ts b/client/src/app/samp/samp.actions.ts index eb290e47161431a877a1b76dd5f99d298f2be7a4..bb4b36ab3931e7786d1fd3b4fe3666b4b697b15e 100644 --- a/client/src/app/samp/samp.actions.ts +++ b/client/src/app/samp/samp.actions.ts @@ -14,3 +14,4 @@ export const registerSuccess = createAction('[Samp] Register Success'); export const registerFail = createAction('[Samp] Register Fail'); export const unregister = createAction('[Samp] Unregister'); export const broadcastVotable = createAction('[Samp] Broadcast Votable', props<{ url: string }>()); +export const broadcastImage = createAction('[Samp] Broadcast Image', props<{ url: string }>()); diff --git a/client/src/app/samp/samp.effects.ts b/client/src/app/samp/samp.effects.ts index 2db3bc713e9c4a035757d7c63310494503c396ef..59327391c5c72a021aa8d86117389915a8f3fdda 100644 --- a/client/src/app/samp/samp.effects.ts +++ b/client/src/app/samp/samp.effects.ts @@ -87,7 +87,7 @@ export class SampEffects { tap(([action, isAuthenticated]) => { if (isAuthenticated) { this.keycloak.getToken().then(token => { - this.sampService.broadcast('table.load.votable', `${action.url}?token=${token}`); + this.sampService.broadcast('table.load.votable', `${action.url}&token=${token}`); }); } else { this.sampService.broadcast('table.load.votable', action.url); @@ -97,6 +97,26 @@ export class SampEffects { { dispatch: false } ); + /** + * Calls actions to broadcast image + */ + broadcastImage$ = createEffect(() => + this.actions$.pipe( + ofType(sampActions.broadcastImage), + concatLatestFrom(() => this.store.select(authSelector.selectIsAuthenticated)), + tap(([action, isAuthenticated]) => { + if (isAuthenticated) { + this.keycloak.getToken().then(token => { + this.sampService.broadcast('image.load.fits', `${action.url}&token=${token}`); + }); + } else { + this.sampService.broadcast('image.load.fits', action.url); + } + }) + ), + { dispatch: false } + ); + constructor( private actions$: Actions, private sampService: SampService, diff --git a/client/yarn.lock b/client/yarn.lock index 7aa60ec7ff6641d3e5f1a3656096d066accfb8e2..6418171403d03c5fd92b58ce44d9bfc89db43592 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2262,6 +2262,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/file-saver@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.5.tgz#9ee342a5d1314bb0928375424a2f162f97c310c7" + integrity sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ== + "@types/geojson@*": version "7946.0.8" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca" @@ -4608,6 +4613,11 @@ figures@^3.0.0: dependencies: escape-string-regexp "^1.0.5" +file-saver@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" + integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" diff --git a/server/app/dependencies.php b/server/app/dependencies.php index 366ed5022924150380976ea3f51456c67f120001..34f5e152b1eb36009e85f66a750802a411715db7 100644 --- a/server/app/dependencies.php +++ b/server/app/dependencies.php @@ -237,18 +237,6 @@ $container->set('App\Action\SearchAction', function (ContainerInterface $c) { ); }); -$container->set('App\Action\StartTaskCreateResultAction', function (ContainerInterface $c) { - return new App\Action\StartTaskCreateResultAction($c->get('em'), $c->get('rmq'), $c->get(SETTINGS)['token']); -}); - -$container->set('App\Action\IsResultAvailableAction', function (ContainerInterface $c) { - return new App\Action\IsResultAvailableAction($c->get('em'), $c->get('settings')['data_path'], $c->get('settings')['result_folder']); -}); - -$container->set('App\Action\DownloadResultAction', function (ContainerInterface $c) { - return new App\Action\DownloadResultAction($c->get('em'), $c->get('settings')['data_path'], $c->get('settings')['result_folder'], $c->get(SETTINGS)['token']); -}); - $container->set('App\Action\StartTaskCreateArchiveAction', function (ContainerInterface $c) { return new App\Action\StartTaskCreateArchiveAction($c->get('em'), $c->get('rmq'), $c->get(SETTINGS)['token']); }); diff --git a/server/app/routes.php b/server/app/routes.php index b4e79e38125c50084438d6f0dff456ba8e19f5a9..19aa875e63d1ba24dda5af9644e69cf9ce82eec2 100644 --- a/server/app/routes.php +++ b/server/app/routes.php @@ -78,11 +78,6 @@ $app->group('', function (RouteCollectorProxy $group) { // Search actions $app->get('/search/{dname}', App\Action\SearchAction::class); -// Result actions -$app->get('/start-task-create-result/{dname}', App\Action\StartTaskCreateResultAction::class); -$app->get('/is-result-available/{id}', App\Action\IsResultAvailableAction::class); -$app->get('/download-result/{dname}/{id}', App\Action\DownloadResultAction::class); - // Archive actions $app->get('/start-task-create-archive/{dname}', App\Action\StartTaskCreateArchiveAction::class); $app->get('/is-archive-available/{id}', App\Action\IsArchiveAvailableAction::class); diff --git a/server/src/Action/DownloadResultAction.php b/server/src/Action/DownloadResultAction.php deleted file mode 100644 index 41a996c7925976d00b187cfc9def5591bab759fb..0000000000000000000000000000000000000000 --- a/server/src/Action/DownloadResultAction.php +++ /dev/null @@ -1,133 +0,0 @@ -<?php - -/* - * This file is part of Anis Server. - * - * (c) Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace App\Action; - -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\ResponseInterface; -use Doctrine\ORM\EntityManagerInterface; -use Slim\Exception\HttpNotFoundException; -use Nyholm\Psr7\Factory\Psr17Factory; - -/** - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action - */ -final class DownloadResultAction extends AbstractAction -{ - /** - * Contains anis-server data path - * - * @var string - */ - private $dataPath; - - /** - * Contains result folder path - * - * @var array - */ - private $resultFolder; - - /** - * Contains settings to handle Json Web Token - * - * @var array - */ - private $settings; - - /** - * Create the classe before call __invoke to execute the action - * - * @param EntityManagerInterface $em Doctrine Entity Manager Interface - * @param string $dataPath Contains anis-server data path - * @param string $resultFolder Contains result folder path - * @param array $settings Settings about token - */ - public function __construct(EntityManagerInterface $em, string $dataPath, string $resultFolder, array $settings) - { - parent::__construct($em); - $this->dataPath = $dataPath; - $this->resultFolder = $resultFolder; - $this->settings = $settings; - } - - /** - * `GET` Returns the file found - * - * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request - * @param ResponseInterface $response PSR-7 This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke( - ServerRequestInterface $request, - ResponseInterface $response, - array $args - ): ResponseInterface { - if ($request->getMethod() === OPTIONS) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - } - - // Search the correct dataset with primary key - $dataset = $this->em->find('App\Entity\Dataset', $args['dname']); - - // If dataset is not found 404 - if (is_null($dataset)) { - throw new HttpNotFoundException( - $request, - 'Dataset with name ' . $args['dname'] . ' is not found' - ); - } - - // If instance is private and authorization enabled - $instance = $dataset->getDatasetFamily()->getInstance(); - if (!$instance->getPublic() && boolval($this->settings['enabled'])) { - $this->verifyInstanceAuthorization( - $request, - $instance->getName(), - explode(',', $this->settings['admin_roles']) - ); - } - - // If dataset is private and authorization enabled - if (!$dataset->getPublic() && boolval($this->settings['enabled'])) { - $this->verifyDatasetAuthorization( - $request, - $dataset->getName(), - explode(',', $this->settings['admin_roles']) - ); - } - - // Search the file - $fileId = $args['id']; - $filePath = $this->dataPath . $this->resultFolder . '/' . $fileId; - - // If the file not found 404 - if (!file_exists($filePath)) { - throw new HttpNotFoundException( - $request, - 'Result file with name ' . $fileId . ' is not found' - ); - } - - // If the file found so stream it - $psr17Factory = new Psr17Factory(); - $stream = $psr17Factory->createStreamFromFile($filePath, 'r'); - - return $response->withBody($stream) - ->withHeader('Content-Disposition', 'attachment; filename=' . basename($filePath) . ';') - ->withHeader('Content-Type', mime_content_type($filePath)) - ->withHeader('Content-Length', filesize($filePath)); - } -} diff --git a/server/src/Action/IsResultAvailableAction.php b/server/src/Action/IsResultAvailableAction.php deleted file mode 100644 index 98019b4aa91f345df3af0b9183e7cf58d78e9abb..0000000000000000000000000000000000000000 --- a/server/src/Action/IsResultAvailableAction.php +++ /dev/null @@ -1,85 +0,0 @@ -<?php - -/* - * This file is part of Anis Server. - * - * (c) Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace App\Action; - -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\ResponseInterface; -use Doctrine\ORM\EntityManagerInterface; - -/** - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action - */ -final class IsResultAvailableAction extends AbstractAction -{ - /** - * Contains anis-server data path - * - * @var string - */ - private $dataPath; - - /** - * Contains result folder path - * - * @var array - */ - private $resultFolder; - - /** - * Create the classe before call __invoke to execute the action - * - * @param EntityManagerInterface $em Doctrine Entity Manager Interface - * @param string $dataPath Contains anis-server data path - * @param string $resultFolder Contains result folder path - */ - public function __construct(EntityManagerInterface $em, string $dataPath, string $resultFolder) - { - parent::__construct($em); - $this->dataPath = $dataPath; - $this->resultFolder = $resultFolder; - } - - /** - * `GET` Returns the file found - * - * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request - * @param ResponseInterface $response PSR-7 This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke( - ServerRequestInterface $request, - ResponseInterface $response, - array $args - ): ResponseInterface { - if ($request->getMethod() === OPTIONS) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - } - - $fileId = $args['id']; - - // Search the file - $filePath = $this->dataPath . $this->resultFolder . '/' . $fileId; - - $isAvailable = false; - if (file_exists($filePath)) { - $isAvailable = true; - } - - $payload = json_encode(array('file_is_available' => $isAvailable)); - $response->getBody()->write($payload); - return $response; - } -} diff --git a/server/src/Action/StartTaskCreateResultAction.php b/server/src/Action/StartTaskCreateResultAction.php deleted file mode 100644 index ebe428496b3ed464bdf0d88bcfc4935ef7b07062..0000000000000000000000000000000000000000 --- a/server/src/Action/StartTaskCreateResultAction.php +++ /dev/null @@ -1,157 +0,0 @@ -<?php - -/* - * This file is part of Anis Server. - * - * (c) Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace App\Action; - -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\ResponseInterface; -use Slim\Exception\HttpBadRequestException; -use Slim\Exception\HttpNotFoundException; -use Doctrine\ORM\EntityManagerInterface; -use PhpAmqpLib\Connection\AbstractConnection; -use PhpAmqpLib\Message\AMQPMessage; - -/** - * @author François Agneray <francois.agneray@lam.fr> - * @package App\Action - */ -final class StartTaskCreateResultAction extends AbstractAction -{ - /** - * Contains RabbitMQ connection socket - * - * @var AbstractConnection - */ - private $rmq; - - /** - * Contains settings to handle Json Web Token (app/settings.php) - * - * @var array - */ - private $settings; - - /** - * Create the classe before call __invoke to execute the action - * - * @param EntityManagerInterface $em Doctrine Entity Manager Interface - * @param AbstractConnection $rmq RabbitMQ connection socket - * @param array $settings Settings about token - */ - public function __construct( - EntityManagerInterface $em, - AbstractConnection $rmq, - array $settings - ) { - parent::__construct($em); - $this->rmq = $rmq; - $this->settings = $settings; - } - - /** - * `GET` Starts an asynchronous task, through rabbitmq, to build an archive - * - * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request - * @param ResponseInterface $response PSR-7 This object represents the HTTP response - * @param string[] $args This table contains information transmitted in the URL (see routes.php) - * - * @return ResponseInterface - */ - public function __invoke( - ServerRequestInterface $request, - ResponseInterface $response, - array $args - ): ResponseInterface { - if ($request->getMethod() === OPTIONS) { - return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - } - - // Search the correct dataset with primary key - $datasetName = $args['dname']; - $dataset = $this->em->find('App\Entity\Dataset', $datasetName); - - // If dataset is not found 404 - if (is_null($dataset)) { - throw new HttpNotFoundException( - $request, - 'Dataset with name ' . $datasetName . ' is not found' - ); - } - - $token = ''; - - // If instance is private and authorization enabled - $instance = $dataset->getDatasetFamily()->getInstance(); - if (!$instance->getPublic() && boolval($this->settings['enabled'])) { - $this->verifyInstanceAuthorization( - $request, - $instance->getName(), - explode(',', $this->settings['admin_roles']) - ); - $token = $request->getHeader('Authorization')[0]; - } - - // If dataset is private and authorization enabled - if (!$dataset->getPublic() && boolval($this->settings['enabled'])) { - $this->verifyDatasetAuthorization( - $request, - $dataset->getName(), - explode(',', $this->settings['admin_roles']) - ); - $token = $request->getHeader('Authorization')[0]; - } - - $queryParams = $request->getQueryParams(); - - // The parameter "a" is mandatory - if (!array_key_exists('a', $queryParams)) { - throw new HttpBadRequestException( - $request, - 'Param a is required for this request' - ); - } - - // Search extension - if ($queryParams['f'] === 'json') { - $extension = '.json'; - } elseif ($queryParams['f'] === 'csv') { - $extension = '.csv'; - } elseif ($queryParams['f'] === 'ascii') { - $extension = '.txt'; - } elseif ($queryParams['f'] === 'votable') { - $extension = '.xml'; - } else { - $extension = '.json'; - } - - // Create the name of the future archive - $fileName = 'result_' . $dataset->getName() . '_' . (new \DateTime())->format('Y-m-d\TH:i:s') . $extension; - $fileId = uniqid(); - - // Publish message in the archive queue - $channel = $this->rmq->channel(); - $channel->queue_declare('archive', false, false, false, false); - - $msg = new AMQPMessage(json_encode(array( - 'file_id' => $fileId, - 'dataset_name' => $datasetName, - 'query' => $request->getUri()->getQuery(), - 'token' => $token - ))); - $channel->basic_publish($msg, '', 'result'); - - // Just returns the future archive name - $payload = json_encode(array('file_name' => $fileName, 'file_id' => $fileId)); - $response->getBody()->write($payload); - return $response; - } -}