From 3bd3ed251e850d805d3650f6b0d57c271e991f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Fri, 9 Jul 2021 22:28:26 +0200 Subject: [PATCH] Cone-search => WIP --- .../components/progress-bar.component.html | 2 +- .../components/result/download.component.html | 4 +- .../components/result/download.component.ts | 17 +- .../search/components/result/index.ts | 10 +- .../components/result/reminder.component.html | 78 ++++++++++ .../components/result/reminder.component.scss | 21 +++ .../components/result/reminder.component.ts | 122 +++++++++++++++ .../components/result/samp.component.html | 32 ++++ .../components/result/samp.component.ts | 47 ++++++ .../result/url-display.component.html | 29 ++++ .../result/url-display.component.ts | 80 ++++++++++ .../search/components/summary.component.ts | 4 +- .../search/containers/result.component.html | 42 ++--- .../search/containers/result.component.ts | 5 +- .../cone-search/cone-search.component.html | 47 ++++++ .../cone-search/cone-search.component.ts | 32 ++++ .../store/actions/cone-search.actions.ts | 0 .../instance/store/effects/search.effects.ts | 71 ++------- .../instance/store/models/criterion.model.ts | 145 ++++++++++++------ .../instance/store/reducers/search.reducer.ts | 18 ++- .../store/selectors/search.selector.ts | 4 +- client/src/app/shared/shared.module.ts | 3 + 22 files changed, 662 insertions(+), 151 deletions(-) create mode 100644 client/src/app/instance/search/components/result/reminder.component.html create mode 100644 client/src/app/instance/search/components/result/reminder.component.scss create mode 100644 client/src/app/instance/search/components/result/reminder.component.ts create mode 100644 client/src/app/instance/search/components/result/samp.component.html create mode 100644 client/src/app/instance/search/components/result/samp.component.ts create mode 100644 client/src/app/instance/search/components/result/url-display.component.html create mode 100644 client/src/app/instance/search/components/result/url-display.component.ts create mode 100644 client/src/app/instance/shared/components/cone-search/cone-search.component.html create mode 100644 client/src/app/instance/shared/components/cone-search/cone-search.component.ts create mode 100644 client/src/app/instance/store/actions/cone-search.actions.ts diff --git a/client/src/app/instance/search/components/progress-bar.component.html b/client/src/app/instance/search/components/progress-bar.component.html index e883f331..4575d0e8 100644 --- a/client/src/app/instance/search/components/progress-bar.component.html +++ b/client/src/app/instance/search/components/progress-bar.component.html @@ -11,7 +11,7 @@ </div> <ul class="nav nav-pills"> <li class="nav-item checked" [ngClass]="{'active': currentStep === 'dataset'}"> - <a class="nav-link" routerLink="/instance/{{ instanceSelected }}/search/dataset/{{ datasetSelected }}" data-toggle="tab"> + <a class="nav-link" routerLink="/instance/{{ instanceSelected }}/search/dataset/{{ datasetSelected }}" [queryParams]="queryParams" data-toggle="tab"> <div class="icon-circle"> <span class="fas fa-book"></span> </div> diff --git a/client/src/app/instance/search/components/result/download.component.html b/client/src/app/instance/search/components/result/download.component.html index b725623a..655ee2e2 100644 --- a/client/src/app/instance/search/components/result/download.component.html +++ b/client/src/app/instance/search/components/result/download.component.html @@ -1,6 +1,4 @@ -<app-spinner *ngIf="(dataLengthIsLoading)"></app-spinner> - -<div *ngIf="(dataLengthIsLoaded)" class="jumbotron mb-4 py-4"> +<div class="jumbotron mb-4 py-4"> <div class="lead"> Dataset <span class="bold">{{ getDatasetLabel() }}</span> selected with <span class="bold">{{ dataLength }}</span> objects found. </div> diff --git a/client/src/app/instance/search/components/result/download.component.ts b/client/src/app/instance/search/components/result/download.component.ts index 5c58b932..aedba808 100644 --- a/client/src/app/instance/search/components/result/download.component.ts +++ b/client/src/app/instance/search/components/result/download.component.ts @@ -7,9 +7,9 @@ * file that was distributed with this source code. */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { Criterion, getCriterionStr } from '../../../store/models'; +import { Criterion, criterionToString } from '../../../store/models'; import { Dataset } from 'src/app/metamodel/models'; import { getHost as host } from 'src/app/shared/utils'; // import { ConeSearch } from '../../../shared/cone-search/store/model'; @@ -25,24 +25,17 @@ import { getHost as host } from 'src/app/shared/utils'; * * @implements OnInit */ -export class DownloadComponent implements OnInit { +export class DownloadComponent { @Input() datasetSelected: string; @Input() datasetList: Dataset[]; @Input() criteriaList: Criterion[]; @Input() outputList: number[]; @Input() dataLength: number; - @Input() dataLengthIsLoading: boolean; - @Input() dataLengthIsLoaded: boolean; //@Input() isConeSearchAdded: boolean; //@Input() coneSearch: ConeSearch; @Input() sampRegistered: boolean; - @Output() getDataLength: EventEmitter<null> = new EventEmitter(); @Output() broadcast: EventEmitter<string> = new EventEmitter(); - ngOnInit() { - this.getDataLength.emit(); - } - /** * Returns dataset label. * @@ -85,7 +78,7 @@ export class DownloadComponent implements OnInit { getUrl(format: string): string { let query: string = host() + '/search/' + this.datasetSelected + '?a=' + this.outputList.join(';'); if (this.criteriaList.length > 0) { - query += '&c=' + this.criteriaList.map(criterion => getCriterionStr(criterion)).join(';'); + query += '&c=' + this.criteriaList.map(criterion => criterionToString(criterion)).join(';'); } // if (this.isConeSearchAdded) { // query += '&cs=' + this.coneSearch.ra + ':' + this.coneSearch.dec + ':' + this.coneSearch.radius; @@ -97,7 +90,7 @@ export class DownloadComponent implements OnInit { getUrlArchive(): string { let query: string = host() + '/archive/' + this.datasetSelected + '?a=' + this.outputList.join(';'); if (this.criteriaList.length > 0) { - query += '&c=' + this.criteriaList.map(criterion => getCriterionStr(criterion)).join(';'); + query += '&c=' + this.criteriaList.map(criterion => criterionToString(criterion)).join(';'); } // if (this.isConeSearchAdded) { // query += '&cs=' + this.coneSearch.ra + ':' + this.coneSearch.dec + ':' + this.coneSearch.radius; diff --git a/client/src/app/instance/search/components/result/index.ts b/client/src/app/instance/search/components/result/index.ts index 2d5a35f2..68174bc2 100644 --- a/client/src/app/instance/search/components/result/index.ts +++ b/client/src/app/instance/search/components/result/index.ts @@ -1,5 +1,11 @@ -import { DownloadComponent } from "./download.component"; +import { DownloadComponent } from './download.component'; +import { ReminderComponent } from './reminder.component'; +import { SampComponent } from './samp.component'; +import { UrlDisplayComponent } from './url-display.component'; export const resultComponents = [ - DownloadComponent + DownloadComponent, + ReminderComponent, + SampComponent, + UrlDisplayComponent ]; diff --git a/client/src/app/instance/search/components/result/reminder.component.html b/client/src/app/instance/search/components/result/reminder.component.html new file mode 100644 index 00000000..d76e57d7 --- /dev/null +++ b/client/src/app/instance/search/components/result/reminder.component.html @@ -0,0 +1,78 @@ +<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="isConeSearchAdded" 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 new file mode 100644 index 00000000..2ae066f5 --- /dev/null +++ b/client/src/app/instance/search/components/result/reminder.component.scss @@ -0,0 +1,21 @@ +/** + * 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.ts b/client/src/app/instance/search/components/result/reminder.component.ts new file mode 100644 index 00000000..9961ece0 --- /dev/null +++ b/client/src/app/instance/search/components/result/reminder.component.ts @@ -0,0 +1,122 @@ +/** + * 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'; + +@Component({ + selector: 'app-reminder', + templateUrl: 'reminder.component.html', + styleUrls: ['reminder.component.scss'] +}) +/** + * @class + * @classdesc Search result reminder component. + */ +export class ReminderComponent { + @Input() datasetSelected: string; + @Input() datasetList: Dataset[]; + @Input() attributeList: Attribute[]; + @Input() criteriaFamilyList: CriteriaFamily[]; + @Input() outputFamilyList: OutputFamily[]; + @Input() outputCategoryList: OutputCategory[]; + // @Input() isConeSearchAdded: boolean; + // @Input() coneSearch: ConeSearch; + @Input() criteriaList: Criterion[]; + @Input() outputList: number[]; + + isSummaryActivated(): boolean { + const dataset = this.datasetList.find(dataset => dataset.name === this.datasetSelected); + return dataset.config.summary.summary_enabled; + } + + isSummaryOpened(): boolean { + const dataset = this.datasetList.find(dataset => dataset.name === this.datasetSelected); + return dataset.config.summary.summary_opened; + } + + /** + * Returns total of added criteria. + * + * @return number + */ + nbCriteria(): number { + // if (this.isConeSearchAdded) { + // 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/samp.component.html b/client/src/app/instance/search/components/result/samp.component.html new file mode 100644 index 00000000..62b9aba7 --- /dev/null +++ b/client/src/app/instance/search/components/result/samp.component.html @@ -0,0 +1,32 @@ +<accordion *ngIf="isSampActivated()" [isAnimated]="true"> + <accordion-group #ag [isOpen]="isSampOpened()" [panelClass]="'custom-accordion'" class="my-2"> + <button class="btn btn-link btn-block clearfix" accordion-heading> + <span class="pull-left float-left"> + <span [style.color]="getColor()"> + <i class="fas fa-circle"></i> + </span> SAMP access + + <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"> + <p *ngIf="!sampRegistered" class="lead"> + You are not connected to a SAMP-hub + </p> + <p *ngIf="sampRegistered" class="lead"> + You are connected to a SAMP-hub + </p> + </div> + <div class="row"> + <button *ngIf="!sampRegistered" (click)="sampRegister.emit()" class="btn btn-outline-primary">Try to register</button> + <button *ngIf="sampRegistered" (click)="sampUnregister.emit()" class="btn btn-outline-primary">Unregister</button> + </div> + </div> + </accordion-group> +</accordion> diff --git a/client/src/app/instance/search/components/result/samp.component.ts b/client/src/app/instance/search/components/result/samp.component.ts new file mode 100644 index 00000000..fb96f57e --- /dev/null +++ b/client/src/app/instance/search/components/result/samp.component.ts @@ -0,0 +1,47 @@ +/** + * 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, ChangeDetectionStrategy, Output, EventEmitter } from '@angular/core'; + +import { Dataset } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-samp', + templateUrl: 'samp.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +/** + * @class + * @classdesc Samp component. + */ +export class SampComponent { + @Input() datasetSelected: string; + @Input() datasetList: Dataset[]; + @Input() sampRegistered: boolean; + @Output() sampRegister: EventEmitter<{}> = new EventEmitter(); + @Output() sampUnregister: EventEmitter<{}> = new EventEmitter(); + + isSampActivated(): boolean { + const dataset = this.datasetList.find(dataset => dataset.name === this.datasetSelected); + return dataset.config.samp.samp_enabled; + } + + isSampOpened(): boolean { + const dataset = this.datasetList.find(dataset => dataset.name === this.datasetSelected); + return dataset.config.samp.samp_opened; + } + + getColor(): string { + if (this.sampRegistered) { + return 'green'; + } else { + return 'red'; + } + } +} 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 new file mode 100644 index 00000000..109b122d --- /dev/null +++ b/client/src/app/instance/search/components/result/url-display.component.html @@ -0,0 +1,29 @@ +<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> 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 new file mode 100644 index 00000000..469bc9b4 --- /dev/null +++ b/client/src/app/instance/search/components/result/url-display.component.ts @@ -0,0 +1,80 @@ +/** + * 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, 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 as host } from 'src/app/shared/utils'; + +@Component({ + selector: 'app-url-display', + templateUrl: 'url-display.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +/** + * @class + * @classdesc Search result URL display component. + */ +export class UrlDisplayComponent { + @Input() datasetSelected: string; + @Input() datasetList: Dataset[]; + // @Input() isConeSearchAdded: boolean; + // @Input() coneSearch: ConeSearch; + @Input() criteriaList: Criterion[]; + @Input() outputList: number[]; + + constructor(private toastr: ToastrService) { } + + /** + * Checks if URL display is enabled. + * + * @return boolean + */ + urlDisplayEnabled(): boolean { + const config = this.datasetList.find(d => d.name === this.datasetSelected).config; + return config.server_link.server_link_enabled; + } + + urlDisplayOpened(): boolean { + const config = this.datasetList.find(d => d.name === this.datasetSelected).config; + return config.server_link.server_link_opened; + } + + /** + * Returns API URL to get data with user parameters. + * + * @return string + */ + getUrl(): string { + let query: string = host() + '/search/' + this.datasetSelected + '?a=' + this.outputList.join(';'); + if (this.criteriaList.length > 0) { + query += '&c=' + this.criteriaList.map(criterion => criterionToString(criterion)).join(';'); + } + // if (this.isConeSearchAdded) { + // 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'); + } +} diff --git a/client/src/app/instance/search/components/summary.component.ts b/client/src/app/instance/search/components/summary.component.ts index 6f7db465..dfa03f44 100644 --- a/client/src/app/instance/search/components/summary.component.ts +++ b/client/src/app/instance/search/components/summary.component.ts @@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core'; -import { Criterion, SearchQueryParams, printCriterion } from '../../store/models'; +import { Criterion, SearchQueryParams, getPrettyCriterion } from '../../store/models'; import { Attribute, Dataset, CriteriaFamily, OutputFamily, OutputCategory } from 'src/app/metamodel/models'; // import { ConeSearch } from '../../shared/cone-search/store/model'; @@ -80,7 +80,7 @@ export class SummaryComponent { * @return string */ printCriterion(criterion: Criterion): string { - return printCriterion(criterion); + return getPrettyCriterion(criterion); } /** diff --git a/client/src/app/instance/search/containers/result.component.html b/client/src/app/instance/search/containers/result.component.html index 009f430b..592300d2 100644 --- a/client/src/app/instance/search/containers/result.component.html +++ b/client/src/app/instance/search/containers/result.component.html @@ -2,14 +2,23 @@ || (attributeListIsLoading | async) || (criteriaFamilyListIsLoading | async) || (outputFamilyListIsLoading | async) - || (outputCategoryListIsLoading | async)"> + || (outputCategoryListIsLoading | async) + || (dataLengthIsLoading | async)"> </app-spinner> +<div *ngIf="(dataLength | async) < 1" class="jumbotron mb-4 py-4"> + <div class="lead"> + No data found for this search + </div> +</div> + <div *ngIf="(datasetListIsLoaded | async) && (attributeListIsLoaded | async) && (criteriaFamilyListIsLoaded | async) && (outputFamilyListIsLoaded | async) - && (outputCategoryListIsLoaded | async)" class="row mt-4"> + && (outputCategoryListIsLoaded | async) + && (dataLengthIsLoaded | async) + && (dataLength | async) > 0" class="row mt-4"> <div class="col-12"> <app-download [datasetSelected]="datasetSelected | async" @@ -17,42 +26,33 @@ [criteriaList]="criteriaList | async" [outputList]="outputList | async" [dataLength]="dataLength | async" - [dataLengthIsLoading]="dataLengthIsLoading | async" - [dataLengthIsLoaded]="dataLengthIsLoaded | async" [sampRegistered]="sampRegistered | async" - (getDataLength)="getDataLength()" (broadcast)="broadcastVotable($event)"> </app-download> - <!-- <app-reminder - [datasetName]="datasetName | async" + <app-reminder + [datasetSelected]="datasetSelected | async" [datasetList]="datasetList | async" - [datasetAttributeList]="attributeList | async" - [dataLengthIsLoaded]="dataLengthIsLoaded | async" - [isConeSearchAdded]="isConeSearchAdded | async" - [coneSearch]="coneSearch | async" + [attributeList]="attributeList | async" [criteriaFamilyList]="criteriaFamilyList | async" - [criteriaList]="criteriaList | async" [outputFamilyList]="outputFamilyList | async" - [categoryList]="categoryList | async" + [outputCategoryList]="outputCategoryList | async" + [criteriaList]="criteriaList | async" [outputList]="outputList | async"> </app-reminder> <app-samp - [datasetName]="datasetName | async" + [datasetSelected]="datasetSelected | async" [datasetList]="datasetList | async" [sampRegistered]="sampRegistered | async" (sampRegister)="sampRegister()" (sampUnregister)="sampUnregister()"> </app-samp> - <app-result-url-display + <app-url-display + [datasetSelected]="datasetSelected | async" [datasetList]="datasetList | async" - [dataLengthIsLoaded]="dataLengthIsLoaded | async" - [datasetName]="datasetName | async" - [isConeSearchAdded]="isConeSearchAdded | async" - [coneSearch]="coneSearch | async" [criteriaList]="criteriaList | async" [outputList]="outputList | async"> - </app-result-url-display> - <app-cone-search-plot-tab + </app-url-display> + <!-- <app-cone-search-plot-tab [datasetName]="datasetName | async" [datasetList]="datasetList | async" [datasetAttributeList]="attributeList | async" diff --git a/client/src/app/instance/search/containers/result.component.ts b/client/src/app/instance/search/containers/result.component.ts index 0b94c8b5..0d4ebb91 100644 --- a/client/src/app/instance/search/containers/result.component.ts +++ b/client/src/app/instance/search/containers/result.component.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; @@ -49,6 +49,7 @@ export class ResultComponent extends AbstractSearchComponent { // This micro task prevent the expression has changed after view init error Promise.resolve(null).then(() => this.store.dispatch(searchActions.changeStep({ step: 'result' }))); Promise.resolve(null).then(() => this.store.dispatch(searchActions.checkResult())); + Promise.resolve(null).then(() => this.store.dispatch(searchActions.retrieveDataLength())); super.ngOnInit(); } @@ -68,7 +69,7 @@ export class ResultComponent extends AbstractSearchComponent { * Dispatches action to retrieve result number. */ getDataLength(): void { - this.store.dispatch(searchActions.retrieveDataLength()); + // this.store.dispatch(searchActions.retrieveDataLength()); } /** diff --git a/client/src/app/instance/shared/components/cone-search/cone-search.component.html b/client/src/app/instance/shared/components/cone-search/cone-search.component.html new file mode 100644 index 00000000..c0cf4330 --- /dev/null +++ b/client/src/app/instance/shared/components/cone-search/cone-search.component.html @@ -0,0 +1,47 @@ +<div class="row pb-4"> + <div class="col"> + <app-resolver + [resolverWip]="resolverWip | async" + [resolver]="resolver | async" + [disabled]="disabled" + (resolveName)="retrieveCoordinates($event)"> + </app-resolver> + </div> +</div> +<div class="row"> + <div class="col pb-4"> + <app-ra + [coneSearch]="coneSearch | async" + [resolver]="resolver | async" + [unit]="unit" + [disabled]="disabled" + (updateConeSearch)="updateConeSearch($event)" + (deleteResolver)="deleteResolver()"> + </app-ra> + </div> + <div class="col-auto p-0 align-self-center"> + <button class="btn btn-outline-secondary" + [disabled]="disabled" + (click)="unit === 'degree' ? unit = 'hms' : unit = 'degree'" + title="Change unit"> + <span class="fas fa-sync-alt"></span> + </button> + </div> + <div class="col"> + <app-dec + [coneSearch]="coneSearch | async" + [resolver]="resolver | async" + [unit]="unit" + [disabled]="disabled" + (updateConeSearch)="updateConeSearch($event)" + (deleteResolver)="deleteResolver()"> + </app-dec> + </div> + <div class="col-12"> + <app-radius + [coneSearch]="coneSearch | async" + [disabled]="disabled" + (updateConeSearch)="updateConeSearch($event)"> + </app-radius> + </div> +</div> diff --git a/client/src/app/instance/shared/components/cone-search/cone-search.component.ts b/client/src/app/instance/shared/components/cone-search/cone-search.component.ts new file mode 100644 index 00000000..d506c318 --- /dev/null +++ b/client/src/app/instance/shared/components/cone-search/cone-search.component.ts @@ -0,0 +1,32 @@ +/** + * 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, Output, EventEmitter } from '@angular/core'; + +import { ConeSearch, Resolver } from 'src/app/instance/store/models'; + +@Component({ + selector: 'app-cone-search', + templateUrl: 'cone-search.component.html' +}) +/** + * @class + * @classdesc Cone search container. + */ +export class ConeSearchComponent { + @Input() disabled: boolean = false; + @Input() resolverWip: boolean; + @Input() resolver: Resolver; + @Input() coneSearch: ConeSearch; + @Output() retrieveCoordinates: EventEmitter<string> = new EventEmitter(); + @Output() updateConeSearch: EventEmitter<ConeSearch> = new EventEmitter(); + @Output() deleteResolver: EventEmitter<{}> = new EventEmitter(); + + unit = 'degree'; +} diff --git a/client/src/app/instance/store/actions/cone-search.actions.ts b/client/src/app/instance/store/actions/cone-search.actions.ts new file mode 100644 index 00000000..e69de29b diff --git a/client/src/app/instance/store/effects/search.effects.ts b/client/src/app/instance/store/effects/search.effects.ts index f400e805..40e09a51 100644 --- a/client/src/app/instance/store/effects/search.effects.ts +++ b/client/src/app/instance/store/effects/search.effects.ts @@ -15,7 +15,7 @@ import { of } from 'rxjs'; import { map, tap, mergeMap, catchError } from 'rxjs/operators'; import { ToastrService } from 'ngx-toastr'; -import { getCriterionStr } from '../models'; +import { criterionToString, stringToCriterion } from '../models'; import { SearchService } from '../services/search.service'; import * as searchActions from '../actions/search.actions'; import * as attributeActions from 'src/app/metamodel/actions/attribute.actions'; @@ -95,72 +95,17 @@ export class SearchEffects { mergeMap(([action, attributeList, criteriaList]) => { let defaultCriteriaList = []; if (criteriaList) { + // Build criteria list with the URL query parameters (c) defaultCriteriaList = criteriaList.split(';').map((c: string) => { const params = c.split('::'); const attribute = attributeList.find(a => a.id === parseInt(params[0], 10)); - switch (attribute.search_type) { - case 'field': - case 'select': - case 'datalist': - case 'radio': - case 'date': - case 'date-time': - case 'time': - return { id: parseInt(params[0], 10), type: 'field', operator: params[1], value: params[2] }; - case 'list': - return { id: parseInt(params[0], 10), type: 'list', values: params[2].split('|') }; - case 'between': - case 'between-date': - if (params[1] === 'bw') { - const bwValues = params[2].split('|'); - return { id: parseInt(params[0], 10), type: 'between', min: bwValues[0], max: bwValues[1] }; - } else if (params[1] === 'gte') { - return { id: parseInt(params[0], 10), type: 'between', min: params[2], max: null }; - } else { - return { id: parseInt(params[0], 10), type: 'between', min: null, max: params[2] }; - } - case 'select-multiple': - case 'checkbox': - const msValues = params[2].split('|'); - const options = attribute.options.filter(option => msValues.includes(option.value)); - return { id: parseInt(params[0], 10), type: 'multiple', options }; - case 'json': - const [path, operator, value] = params[2].split('|'); - return { id: parseInt(params[0], 10), type: 'json', path, operator, value }; - default: - return null; - } + return stringToCriterion(attribute, params); }); } else { + // Build default criteria list with the attribute list metamodel configuration defaultCriteriaList = attributeList .filter(attribute => attribute.id_criteria_family && attribute.search_type && attribute.min) - .map(attribute => { - switch (attribute.search_type) { - case 'field': - case 'select': - case 'datalist': - case 'radio': - case 'date': - case 'date-time': - case 'time': - return { id: attribute.id, type: 'field', value: attribute.min.toString(), operator: attribute.operator }; - case 'list': - return { id: attribute.id, type: 'list', values: attribute.min.toString().split('|') }; - case 'between': - case 'between-date': - return { id: attribute.id, type: 'between', min: attribute.min.toString(), max: attribute.max.toString() }; - case 'select-multiple': - case 'checkbox': - const msValues = attribute.min.toString().split('|'); - const options = attribute.options.filter(option => msValues.includes(option.value)); - return { id: attribute.id, type: 'multiple', options }; - case 'json': - const [path, operator, value] = attribute.min.toString().split('|'); - return { id: attribute.id, type: 'json', path, operator, value }; - default: - return null; - } - }); + .map(attribute => stringToCriterion(attribute)); } return of(searchActions.updateCriteriaList({ criteriaList: defaultCriteriaList })); }) @@ -177,8 +122,10 @@ export class SearchEffects { mergeMap(([action, attributeList, outputList]) => { let defaultOutputList = []; if (outputList) { + // Build output list with the URL query parameters (a) defaultOutputList = outputList.split(';').map((o: string) => parseInt(o, 10)); } else { + // Build default output list with the attribute list metamodel configuration defaultOutputList = attributeList .filter(attribute => attribute.selected && attribute.id_output_category) .map(attribute => attribute.id); @@ -198,7 +145,7 @@ export class SearchEffects { mergeMap(([action, datasetName, criteriaList]) => { let query = datasetName + '?a=count'; if (criteriaList.length > 0) { - query += '&c=' + criteriaList.map(criterion => getCriterionStr(criterion)).join(';'); + query += '&c=' + criteriaList.map(criterion => criterionToString(criterion)).join(';'); } return this.searchService.retrieveDataLength(query) @@ -228,7 +175,7 @@ export class SearchEffects { mergeMap(([action, datasetName, criteriaList, outputList]) => { let query = datasetName + '?a=' + outputList.join(';'); if (criteriaList.length > 0) { - query += '&c=' + criteriaList.map(criterion => getCriterionStr(criterion)).join(';'); + query += '&c=' + criteriaList.map(criterion => criterionToString(criterion)).join(';'); } query += '&p=' + action.pagination.nbItems + ':' + action.pagination.page; query += '&o=' + action.pagination.sortedCol + ':' + action.pagination.order; diff --git a/client/src/app/instance/store/models/criterion.model.ts b/client/src/app/instance/store/models/criterion.model.ts index d9e8c797..1b2f328a 100644 --- a/client/src/app/instance/store/models/criterion.model.ts +++ b/client/src/app/instance/store/models/criterion.model.ts @@ -7,13 +7,14 @@ * file that was distributed with this source code. */ - import { +import { BetweenCriterion, FieldCriterion, JsonCriterion, SelectMultipleCriterion, ListCriterion } from './criterion'; +import { Attribute } from 'src/app/metamodel/models'; /** * @class @@ -24,55 +25,17 @@ export interface Criterion { type: string; } -/** - * Returns pretty criterion string. - * - * @param {Criterion} criterion - The criterion to pretty print. - * - * @return string - * - * @example - * {{ printCriterion(criterion) }} - */ - export const printCriterion = (criterion: Criterion): string => { - switch (criterion.type) { - case 'between': - const bw = criterion as BetweenCriterion; - if (bw.min === null) { - return '<= ' + bw.max; - } else if (bw.max === null) { - return '>= ' + bw.min; - } else { - return '∈ [' + bw.min + ';' + bw.max + ']'; - } - case 'field': - const fd = criterion as FieldCriterion; - return getPrettyOperator(fd.operator) + ' ' + fd.value.split('|').join(', '); - case 'list': - const ls = criterion as ListCriterion; - return '= [' + ls.values.join(',') + ']'; - case 'json' : - const json = criterion as JsonCriterion; - return json.path + ' ' + json.operator + ' ' + json.value; - case 'multiple': - const multiple = criterion as SelectMultipleCriterion; - return '[' + multiple.options.map(option => option.label).join(',') + ']'; - default: - return 'Criterion type not valid!'; - } -} - /** * Returns criterion notation for Anis Server. * - * @param {Criterion} criterion - The criterion to pretty print. + * @param {Criterion} criterion - The criterion to transform. * * @return string * * @example - * getCriterionStr(criterion) + * criterionToString(criterion) */ -export const getCriterionStr = (criterion: Criterion): string => { +export const criterionToString = (criterion: Criterion): string => { let str: string = criterion.id.toString(); if (criterion.type === 'between') { const bw = criterion as BetweenCriterion; @@ -103,6 +66,102 @@ export const getCriterionStr = (criterion: Criterion): string => { return str; } +export const stringToCriterion = (attribute: Attribute, params: string[] = null): Criterion => { + switch (attribute.search_type) { + case 'field': + case 'select': + case 'datalist': + case 'radio': + case 'date': + case 'date-time': + case 'time': + if (params) { + return { id: attribute.id, type: 'field', operator: params[1], value: params[2] } as FieldCriterion; + } else { + return { id: attribute.id, type: 'field', operator: attribute.operator, value: attribute.min.toString() } as FieldCriterion; + } + case 'list': + if (params) { + return { id: attribute.id, type: 'list', values: params[2].split('|') } as ListCriterion; + } else { + return { id: attribute.id, type: 'list', values: attribute.min.toString().split('|') } as ListCriterion; + } + case 'between': + case 'between-date': + if (params) { + if (params[1] === 'bw') { + const bwValues = params[2].split('|'); + return { id: attribute.id, type: 'between', min: bwValues[0], max: bwValues[1] } as BetweenCriterion; + } else if (params[1] === 'gte') { + return { id: attribute.id, type: 'between', min: params[2], max: null } as BetweenCriterion; + } else { + return { id: attribute.id, type: 'between', min: null, max: params[2] } as BetweenCriterion; + } + } else { + return { id: attribute.id, type: 'between', min: attribute.min.toString(), max: attribute.max.toString() } as BetweenCriterion; + } + case 'select-multiple': + case 'checkbox': + if (params) { + const msValues = params[2].split('|'); + const options = attribute.options.filter(option => msValues.includes(option.value)); + return { id: attribute.id, type: 'multiple', options } as SelectMultipleCriterion; + } else { + const msValues = attribute.min.toString().split('|'); + const options = attribute.options.filter(option => msValues.includes(option.value)); + return { id: attribute.id, type: 'multiple', options } as SelectMultipleCriterion; + } + case 'json': + if (params) { + const [path, operator, value] = params[2].split('|'); + return { id: attribute.id, type: 'json', path, operator, value } as JsonCriterion; + } else { + const [path, operator, value] = attribute.min.toString().split('|'); + return { id: attribute.id, type: 'json', path, operator, value } as JsonCriterion; + } + default: + return null; + } +} + +/** + * Returns pretty criterion string. + * + * @param {Criterion} criterion - The criterion to pretty print. + * + * @return string + * + * @example + * {{ printCriterion(criterion) }} + */ +export const getPrettyCriterion = (criterion: Criterion): string => { + switch (criterion.type) { + case 'between': + const bw = criterion as BetweenCriterion; + if (bw.min === null) { + return '<= ' + bw.max; + } else if (bw.max === null) { + return '>= ' + bw.min; + } else { + return '∈ [' + bw.min + ';' + bw.max + ']'; + } + case 'field': + const fd = criterion as FieldCriterion; + return getPrettyOperator(fd.operator) + ' ' + fd.value.split('|').join(', '); + case 'list': + const ls = criterion as ListCriterion; + return '= [' + ls.values.join(',') + ']'; + case 'json' : + const json = criterion as JsonCriterion; + return json.path + ' ' + json.operator + ' ' + json.value; + case 'multiple': + const multiple = criterion as SelectMultipleCriterion; + return '[' + multiple.options.map(option => option.label).join(',') + ']'; + default: + return 'Criterion type not valid!'; + } +} + /** * Returns an Anis Server string operator to a pretty form label operator. * @@ -114,7 +173,7 @@ export const getCriterionStr = (criterion: Criterion): string => { * // returns = * getPrettyOperator('eq') */ - const getPrettyOperator = (operator: string): string => { +const getPrettyOperator = (operator: string): string => { switch (operator) { case 'eq': return '='; diff --git a/client/src/app/instance/store/reducers/search.reducer.ts b/client/src/app/instance/store/reducers/search.reducer.ts index 94650279..66221436 100644 --- a/client/src/app/instance/store/reducers/search.reducer.ts +++ b/client/src/app/instance/store/reducers/search.reducer.ts @@ -100,13 +100,29 @@ export const searchReducer = createReducer( ...state, selectedData: [...state.selectedData.filter(d => d !== id)] })), + on(searchActions.retrieveDataLength, state => ({ + ...state, + dataLengthIsLoading: true, + dataLengthIsLoaded: false + })), + on(searchActions.retrieveDataLengthSuccess, (state, { length }) => ({ + ...state, + dataLength: length, + dataLengthIsLoading: false, + dataLengthIsLoaded: true + })), + on(searchActions.retrieveDataLengthFail, state => ({ + ...state, + dataLengthIsLoading: false + })), on(searchActions.destroyResults, state => ({ ...state, searchData: [], dataLength: null })), on(searchActions.resetSearch, () => ({ - ...initialState + ...initialState, + currentStep: 'dataset' })) ); diff --git a/client/src/app/instance/store/selectors/search.selector.ts b/client/src/app/instance/store/selectors/search.selector.ts index af427be8..57c9c1ba 100644 --- a/client/src/app/instance/store/selectors/search.selector.ts +++ b/client/src/app/instance/store/selectors/search.selector.ts @@ -9,7 +9,7 @@ import { createSelector } from '@ngrx/store'; -import { Criterion, SearchQueryParams, getCriterionStr } from '../models'; +import { Criterion, SearchQueryParams, criterionToString } from '../models'; import * as reducer from '../../instance.reducer'; import * as fromSearch from '../reducers/search.reducer'; @@ -111,7 +111,7 @@ export const selectQueryParams = createSelector( if (criteriaList.length > 0) { queryParams = { ...queryParams, - c: criteriaList.map(criterion => getCriterionStr(criterion)).join(';') + c: criteriaList.map(criterion => criterionToString(criterion)).join(';') }; } return queryParams; diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 1975b5c2..9ba0031b 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -20,6 +20,7 @@ import { AccordionModule } from 'ngx-bootstrap/accordion'; import { PopoverModule } from 'ngx-bootstrap/popover'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; +import { TabsModule } from 'ngx-bootstrap/tabs'; import { NgSelectModule } from '@ng-select/ng-select'; import { sharedComponents } from './components'; @@ -43,6 +44,7 @@ import { sharedPipes } from './pipes'; PopoverModule.forRoot(), TooltipModule.forRoot(), BsDatepickerModule.forRoot(), + TabsModule.forRoot(), NgSelectModule ], exports: [ @@ -57,6 +59,7 @@ import { sharedPipes } from './pipes'; PopoverModule, TooltipModule, BsDatepickerModule, + TabsModule, NgSelectModule, sharedComponents, sharedPipes -- GitLab