diff --git a/client/.gitlab-ci.yml b/client/.gitlab-ci.yml index f803db047618684ee122a8bb17322c98f4d9dd8f..471927d9567a05ac6c960c17f1ee9be770595e96 100644 --- a/client/.gitlab-ci.yml +++ b/client/.gitlab-ci.yml @@ -4,6 +4,7 @@ stages: - sonar - build - dockerize + - deploy variables: VERSION: "3.7.0" @@ -81,3 +82,17 @@ dockerize: - docker pull $CI_REGISTRY/anis/anis-next/client:latest || true - docker build --cache-from $CI_REGISTRY/anis/anis-next/client:latest -t $CI_REGISTRY/anis/anis-next/client:latest . - docker push $CI_REGISTRY/anis/anis-next/client:latest + +deploy: + image: alpine + stage: deploy + variables: + GIT_STRATEGY: none + cache: {} + dependencies: [] + script: + - apk add --update curl + - curl -XPOST $DEV_WEBHOOK_CLIENT + only: + refs: + - develop \ No newline at end of file diff --git a/client/src/app/admin/admin-auth.guard.ts b/client/src/app/admin/admin-auth.guard.ts index 02ac7a5e6dea1d5f36f8abb33b1a912d2c75d9e5..2658bb975516deac79518857d4d126a3636efdbf 100644 --- a/client/src/app/admin/admin-auth.guard.ts +++ b/client/src/app/admin/admin-auth.guard.ts @@ -9,8 +9,8 @@ import { Injectable } from '@angular/core'; import { CanActivate, Router } from '@angular/router'; -import { Store, select } from '@ngrx/store'; +import { Store, select } from '@ngrx/store'; import { combineLatest, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; diff --git a/client/src/app/admin/components/dataset/dataset-form.component.html b/client/src/app/admin/components/dataset/dataset-form.component.html index 1fd269376ba2d3fa1a15e3bc7343f840f0644f8b..de298ce4182901d61df57cfe5f9b24183f0192ad 100644 --- a/client/src/app/admin/components/dataset/dataset-form.component.html +++ b/client/src/app/admin/components/dataset/dataset-form.component.html @@ -45,7 +45,7 @@ [rootDirectory]="rootDirectory" [rootDirectoryIsLoading]="rootDirectoryIsLoading" [rootDirectoryIsLoaded]="rootDirectoryIsLoaded" - (loadRootDirectory)="loadRootDirectory.emit($event)"> + (loadRootDirectory)="onChangeDataPath($event)"> </app-data-path-form-control> <div class="form-group"> <label for="display">Display</label> @@ -60,6 +60,7 @@ <label class="custom-control-label" for="private"><i class="fas fa-lock"></i> Private</label> </div> </accordion-group> + <app-info-survey-form-group [form]="infoSurveyFormGroup"></app-info-survey-form-group> <app-image-form-group [form]="imageFormGroup" [isDisabled]="isNewDataset"></app-image-form-group> <app-cone-search-form-group [form]="coneSearchFormGroup" [isDisabled]="isNewDataset" [attributeList]="attributeList"></app-cone-search-form-group> <app-download-form-group [form]="downloadFormGroup"></app-download-form-group> diff --git a/client/src/app/admin/components/dataset/dataset-form.component.ts b/client/src/app/admin/components/dataset/dataset-form.component.ts index bf60f446235962f3101a7f97409400fb74196efd..db9f0b6f2a2c5335dddb6786d640081e0372482f 100644 --- a/client/src/app/admin/components/dataset/dataset-form.component.ts +++ b/client/src/app/admin/components/dataset/dataset-form.component.ts @@ -10,13 +10,14 @@ import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; -import { Dataset, Survey, DatasetFamily, FileInfo, Attribute } from 'src/app/metamodel/models'; +import { Instance, Dataset, Survey, DatasetFamily, FileInfo, Attribute } from 'src/app/metamodel/models'; @Component({ selector: 'app-dataset-form', templateUrl: 'dataset-form.component.html' }) export class DatasetFormComponent implements OnInit, OnChanges { + @Input() instance: Instance; @Input() dataset: Dataset; @Input() surveyList: Survey[]; @Input() tableListIsLoading: boolean; @@ -34,6 +35,11 @@ export class DatasetFormComponent implements OnInit, OnChanges { public isNewDataset = true; + public infoSurveyFormGroup = new FormGroup({ + survey_enabled: new FormControl(false), + survey_label: new FormControl('More about this survey', [Validators.required]) + }); + public imageFormGroup = new FormGroup({ }); @@ -90,6 +96,7 @@ export class DatasetFormComponent implements OnInit, OnChanges { display: new FormControl('', [Validators.required]), public: new FormControl('', [Validators.required]), config: new FormGroup({ + survey: this.infoSurveyFormGroup, cone_search: this.coneSearchFormGroup, download: this.downloadFormGroup, summary: this.summaryFormGroup, @@ -137,4 +144,8 @@ export class DatasetFormComponent implements OnInit, OnChanges { this.changeSurvey.emit(this.surveyList.find(survey => survey.name === surveyName).id_database); } } + + onChangeDataPath(path: string) { + this.loadRootDirectory.emit(this.instance.data_path + path); + } } diff --git a/client/src/app/admin/components/dataset/index.ts b/client/src/app/admin/components/dataset/index.ts index da36d39910a4ed5d8a212d899df6302795d74c8c..aad69881885c74aa8e385059e914a4ab6d0ed481 100644 --- a/client/src/app/admin/components/dataset/index.ts +++ b/client/src/app/admin/components/dataset/index.ts @@ -9,7 +9,7 @@ import { DatasetCardComponent } from './dataset-card.component'; import { DatasetFormComponent } from './dataset-form.component'; -import { DataPathFormControlComponent } from './data-path-form-control.component'; +import { InfoSurveyFormGroupComponent } from './info-survey-form-group.component'; import { ImageFormGroupComponent } from './image-form-group.component'; import { ConeSearchFormGroupComponent } from './cone-search-form-group.component'; import { DownloadFormGroupComponent } from './download-form-group.component'; @@ -21,7 +21,7 @@ import { DatatableFormGroupComponent } from './datatable-form-group.component'; export const datasetComponents = [ DatasetCardComponent, DatasetFormComponent, - DataPathFormControlComponent, + InfoSurveyFormGroupComponent, ImageFormGroupComponent, ConeSearchFormGroupComponent, DownloadFormGroupComponent, diff --git a/client/src/app/admin/components/dataset/info-survey-form-group.component.html b/client/src/app/admin/components/dataset/info-survey-form-group.component.html new file mode 100644 index 0000000000000000000000000000000000000000..df4f4f612c48f2d84f72299ed58d689b96c60b64 --- /dev/null +++ b/client/src/app/admin/components/dataset/info-survey-form-group.component.html @@ -0,0 +1,12 @@ +<form [formGroup]="form" novalidate> + <accordion-group heading="Info survey"> + <div class="custom-control custom-switch"> + <input class="custom-control-input" type="checkbox" id="survey_enabled" name="survey_enabled" formControlName="survey_enabled"> + <label class="custom-control-label" for="survey_enabled">Enabled</label> + </div> + <div class="form-group"> + <label for="survey_label">Label</label> + <input type="text" class="form-control" id="survey_label" name="survey_label" formControlName="survey_label"> + </div> + </accordion-group> +</form> \ No newline at end of file diff --git a/client/src/app/admin/components/dataset/info-survey-form-group.component.ts b/client/src/app/admin/components/dataset/info-survey-form-group.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e2ae468b15fcca7274e0c5d3791610f7f3d584c --- /dev/null +++ b/client/src/app/admin/components/dataset/info-survey-form-group.component.ts @@ -0,0 +1,19 @@ +/** + * 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 { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'app-info-survey-form-group', + templateUrl: 'info-survey-form-group.component.html' +}) +export class InfoSurveyFormGroupComponent { + @Input() form: FormGroup; +} diff --git a/client/src/app/admin/components/instance/design-form-group.component.html b/client/src/app/admin/components/instance/design-form-group.component.html index be9ed62b499b9719d3d299e38ec3f022c400efa8..7acdef818abaf83e53e31bbf92d84e168bc120ef 100644 --- a/client/src/app/admin/components/instance/design-form-group.component.html +++ b/client/src/app/admin/components/instance/design-form-group.component.html @@ -1,16 +1,34 @@ <form [formGroup]="form" novalidate> <accordion-group heading="Design"> - <div class="form-row"> <div class="form-group col-md-6"> - <label for="design_color_picker">Color picker</label> + <label for="design_color_picker">Instance color (picker)</label> <input class="form-control" type="color" id="design_color_picker" [value]="form.value.design_color" formControlName="design_color"> </div> <div class="form-group col-md-6"> - <label for="design_color_input">Color value</label> + <label for="design_color_input">Instance color (value)</label> <input type="text" class="form-control" id="design_color_input" [value]="form.value.design_color" formControlName="design_color"> </div> </div> - + <app-file-select-form-control + [form]="form" + [disabled]="dataPathEmpty" + [controlName]="'design_logo'" + [controlLabel]="'Logo'" + [rootDirectory]="rootDirectory" + [rootDirectoryIsLoading]="rootDirectoryIsLoading" + [rootDirectoryIsLoaded]="rootDirectoryIsLoaded" + (loadRootDirectory)="loadRootDirectory.emit($event)"> + </app-file-select-form-control> + <app-file-select-form-control + [form]="form" + [disabled]="dataPathEmpty" + [controlName]="'design_favicon'" + [controlLabel]="'Favicon'" + [rootDirectory]="rootDirectory" + [rootDirectoryIsLoading]="rootDirectoryIsLoading" + [rootDirectoryIsLoaded]="rootDirectoryIsLoaded" + (loadRootDirectory)="loadRootDirectory.emit($event)"> + </app-file-select-form-control> </accordion-group> </form> diff --git a/client/src/app/admin/components/instance/design-form-group.component.ts b/client/src/app/admin/components/instance/design-form-group.component.ts index 2c84d3c1c0e368851efaa05701fbfee90e03b1f9..aa233db08a9269146caae2d9a77fbc74de94596b 100644 --- a/client/src/app/admin/components/instance/design-form-group.component.ts +++ b/client/src/app/admin/components/instance/design-form-group.component.ts @@ -7,13 +7,20 @@ * file that was distributed with this source code. */ -import { Component, Input } from '@angular/core'; +import { Component, Input, Output, EventEmitter } from '@angular/core'; import { FormGroup } from '@angular/forms'; +import { FileInfo } from 'src/app/metamodel/models'; + @Component({ selector: 'app-design-form-group', templateUrl: 'design-form-group.component.html' }) export class DesignFormGroupComponent { @Input() form: FormGroup; + @Input() dataPathEmpty: boolean; + @Input() rootDirectory: FileInfo[]; + @Input() rootDirectoryIsLoading: boolean; + @Input() rootDirectoryIsLoaded: boolean; + @Output() loadRootDirectory: EventEmitter<string> = new EventEmitter(); } diff --git a/client/src/app/admin/components/instance/documentation-form-group.component.html b/client/src/app/admin/components/instance/documentation-form-group.component.html index 43949bec841aef071475600ecf28745d50fb0a4f..4af970bf0aaff013360b5a39392a01d3e5961b80 100644 --- a/client/src/app/admin/components/instance/documentation-form-group.component.html +++ b/client/src/app/admin/components/instance/documentation-form-group.component.html @@ -1,8 +1,12 @@ <form [formGroup]="form" novalidate> <accordion-group heading="Documentation"> - <div class="custom-control custom-switch"> - <input class="custom-control-input" type="checkbox" id="documentation_allowed" name="documentation_allowed" formControlName="documentation_allowed"> + <div class="custom-control custom-switch mb-2"> + <input class="custom-control-input" type="checkbox" id="documentation_allowed" name="documentation_allowed" formControlName="documentation_allowed" (change)="checkDisableDocumentationAllowed()"> <label class="custom-control-label" for="documentation_allowed">Documentation allowed</label> </div> + <div class="form-group"> + <label for="documentation_label">Label</label> + <input type="text" class="form-control" id="documentation_label" formControlName="documentation_label"> + </div> </accordion-group> </form> \ No newline at end of file diff --git a/client/src/app/admin/components/instance/documentation-form-group.component.ts b/client/src/app/admin/components/instance/documentation-form-group.component.ts index 5e69fa7e73d3621dbc284c060087619149fefe44..3355e39992fb470a29080bde244c33f9df32b144 100644 --- a/client/src/app/admin/components/instance/documentation-form-group.component.ts +++ b/client/src/app/admin/components/instance/documentation-form-group.component.ts @@ -16,4 +16,12 @@ import { FormGroup } from '@angular/forms'; }) export class DocumentationFormGroupComponent { @Input() form: FormGroup; + + checkDisableDocumentationAllowed() { + if (this.form.controls.documentation_allowed.value) { + this.form.controls.documentation_label.enable(); + } else { + this.form.controls.documentation_label.disable(); + } + } } diff --git a/client/src/app/admin/components/instance/home-form-group.component.html b/client/src/app/admin/components/instance/home-form-group.component.html new file mode 100644 index 0000000000000000000000000000000000000000..8ceb5c69277bd6f0727c0f481f0aa61f49d6bf33 --- /dev/null +++ b/client/src/app/admin/components/instance/home-form-group.component.html @@ -0,0 +1,25 @@ +<form [formGroup]="form" novalidate> + <accordion-group heading="Home"> + <div class="form-group"> + <label for="home_component">Component</label> + <select class="form-control" name="home_component" formControlName="home_component"> + <option value="WelcomeComponent">WelcomeComponent</option> + </select> + </div> + <div formGroupName="home_config"> + <div class="form-group"> + <label for="home_component_text">Text</label> + <textarea class="form-control" id="home_component_text" formControlName="home_component_text" rows="3"></textarea> + </div> + <app-file-select-form-control + [form]="getHomeConfigFormGroup()" + [controlName]="'home_component_logo'" + [controlLabel]="'Logo'" + [rootDirectory]="rootDirectory" + [rootDirectoryIsLoading]="rootDirectoryIsLoading" + [rootDirectoryIsLoaded]="rootDirectoryIsLoaded" + (loadRootDirectory)="loadRootDirectory.emit($event)"> + </app-file-select-form-control> + </div> + </accordion-group> +</form> diff --git a/client/src/app/admin/components/instance/home-form-group.component.ts b/client/src/app/admin/components/instance/home-form-group.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..df09361e62d91271ad60b5ec0b94579deeebfd48 --- /dev/null +++ b/client/src/app/admin/components/instance/home-form-group.component.ts @@ -0,0 +1,29 @@ +/** + * 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 { FormGroup } from '@angular/forms'; + +import { FileInfo } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-home-form-group', + templateUrl: 'home-form-group.component.html' +}) +export class HomeFormGroupComponent { + @Input() form: FormGroup; + @Input() rootDirectory: FileInfo[]; + @Input() rootDirectoryIsLoading: boolean; + @Input() rootDirectoryIsLoaded: boolean; + @Output() loadRootDirectory: EventEmitter<string> = new EventEmitter(); + + getHomeConfigFormGroup() { + return this.form.controls.home_config as FormGroup; + } +} diff --git a/client/src/app/admin/components/instance/index.ts b/client/src/app/admin/components/instance/index.ts index 47997603644fc0f5c165dc8aad02983b360ae23d..f3c08b32be425bd190195ef569961e962ca9a188 100644 --- a/client/src/app/admin/components/instance/index.ts +++ b/client/src/app/admin/components/instance/index.ts @@ -11,6 +11,7 @@ import { InstanceCardComponent } from './instance-card.component'; import { InstanceButtonsComponent } from './instance-buttons.component'; import { InstanceFormComponent } from './instance-form.component'; import { DesignFormGroupComponent } from './design-form-group.component'; +import { HomeFormGroupComponent } from './home-form-group.component'; import { SearchFormGroupComponent } from './search-form-group.component'; import { DocumentationFormGroupComponent } from './documentation-form-group.component'; @@ -19,6 +20,7 @@ export const instanceComponents = [ InstanceButtonsComponent, InstanceFormComponent, DesignFormGroupComponent, + HomeFormGroupComponent, SearchFormGroupComponent, DocumentationFormGroupComponent ]; diff --git a/client/src/app/admin/components/instance/instance-card.component.html b/client/src/app/admin/components/instance/instance-card.component.html index e1fd448d4941c2e475df22c2b701002011bd379e..9c9aa4fa036fb5da6a51b7fd7368e7cff0745579 100644 --- a/client/src/app/admin/components/instance/instance-card.component.html +++ b/client/src/app/admin/components/instance/instance-card.component.html @@ -4,7 +4,6 @@ <ul class="card-text list-unstyled pl-4"> <li>Dataset families: <span class="badge badge-secondary">{{ instance.nb_dataset_families }}</span></li> <li>Datasets: <span class="badge badge-secondary">{{ instance.nb_datasets }}</span></li> - <li><a target="blank" href="{{instance.client_url}}">{{instance.client_url}}</a></li> </ul> </div> <div class="card-footer bg-transparent text-right"> diff --git a/client/src/app/admin/components/instance/instance-form.component.html b/client/src/app/admin/components/instance/instance-form.component.html index abf027b3ad2fc6e116075cf6d9897837fa4a37bc..bf9b591dfd0280ef119cdb89144d90c5f7247af5 100644 --- a/client/src/app/admin/components/instance/instance-form.component.html +++ b/client/src/app/admin/components/instance/instance-form.component.html @@ -3,18 +3,35 @@ <accordion-group heading="General information" [isOpen]="true"> <div class="form-group"> <label for="name">Name</label> - <input id="name" type="text" class="form-control" id="name" name="name" formControlName="name"> + <input type="text" class="form-control" id="name" name="name" formControlName="name"> </div> <div class="form-group"> <label for="label">Label</label> - <input id="label" type="text" class="form-control" id="label" name="label" formControlName="label"> - </div> - <div class="form-group"> - <label for="client_url">Client URL</label> - <input id="client_url" type="text" class="form-control" id="client_url" name="client_url" formControlName="client_url"> + <input type="text" class="form-control" id="label" name="label" formControlName="label"> </div> + <app-data-path-form-control + [form]="form" + [rootDirectory]="rootDirectory" + [rootDirectoryIsLoading]="rootDirectoryIsLoading" + [rootDirectoryIsLoaded]="rootDirectoryIsLoaded" + (loadRootDirectory)="loadRootDirectory.emit($event)"> + </app-data-path-form-control> </accordion-group> - <app-design-form-group [form]="designFormGroup"></app-design-form-group> + <app-design-form-group + [form]="designFormGroup" + [dataPathEmpty]="isDataPathEmpty()" + [rootDirectory]="rootDirectory" + [rootDirectoryIsLoading]="rootDirectoryIsLoading" + [rootDirectoryIsLoaded]="rootDirectoryIsLoaded" + (loadRootDirectory)="onChangeSelectFile($event)"> + </app-design-form-group> + <app-home-form-group + [form]="homeFormGroup" + [rootDirectory]="rootDirectory" + [rootDirectoryIsLoading]="rootDirectoryIsLoading" + [rootDirectoryIsLoaded]="rootDirectoryIsLoaded" + (loadRootDirectory)="onChangeSelectFile($event)"> + </app-home-form-group> <app-search-form-group [form]="searchFormGroup"></app-search-form-group> <app-documentation-form-group [form]="documentationFormGroup"></app-documentation-form-group> </accordion> diff --git a/client/src/app/admin/components/instance/instance-form.component.ts b/client/src/app/admin/components/instance/instance-form.component.ts index 1463bfa2ef850b440bc79fc8d76c7a515bb94242..7ff24f3c62be53b8583e0328b4fec06f86ebd9db 100644 --- a/client/src/app/admin/components/instance/instance-form.component.ts +++ b/client/src/app/admin/components/instance/instance-form.component.ts @@ -10,7 +10,7 @@ import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; -import { Instance } from 'src/app/metamodel/models'; +import { Instance, FileInfo } from 'src/app/metamodel/models'; @Component({ selector: 'app-instance-form', @@ -18,28 +18,48 @@ import { Instance } from 'src/app/metamodel/models'; }) export class InstanceFormComponent implements OnInit { @Input() instance: Instance; + @Input() rootDirectory: FileInfo[]; + @Input() rootDirectoryIsLoading: boolean; + @Input() rootDirectoryIsLoaded: boolean; + @Output() loadRootDirectory: EventEmitter<string> = new EventEmitter(); @Output() onSubmit: EventEmitter<Instance> = new EventEmitter(); public designFormGroup = new FormGroup({ - design_color: new FormControl('', [Validators.required]) + design_color: new FormControl('#7AC29A', [Validators.required]), + design_logo: new FormControl(''), + design_favicon: new FormControl('') + }); + + public homeFormGroup = new FormGroup({ + home_component: new FormControl('WelcomeComponent', [Validators.required]), + home_config: new FormGroup({ + home_component_text: new FormControl(`AstroNomical Information System is a generic web tool aimed +at facilitating and homogenizing the implementation of astronomical data. It allows +the fast implementation of a project data exchange platform in a dedicated information system.`, [Validators.required]), + home_component_logo: new FormControl('home_component_logo.png', [Validators.required]) + }) }); public searchFormGroup = new FormGroup({ search_by_criteria_allowed: new FormControl(true), + search_by_criteria_label: new FormControl({value: 'Search', disabled: false}), search_multiple_allowed: new FormControl(false), + search_multiple_label: new FormControl({value: 'Search multiple', disabled: true}), search_multiple_all_datasets_selected: new FormControl({value: false, disabled: true}) }); public documentationFormGroup = new FormGroup({ - documentation_allowed: new FormControl(false) + documentation_allowed: new FormControl(false), + documentation_label: new FormControl({value: 'Documentation', disabled: true}) }); public form = new FormGroup({ name: new FormControl('', [Validators.required]), label: new FormControl('', [Validators.required]), - client_url: new FormControl('', [Validators.required]), + data_path: new FormControl(''), config: new FormGroup({ design: this.designFormGroup, + home: this.homeFormGroup, search: this.searchFormGroup, documentation: this.documentationFormGroup }) @@ -48,12 +68,28 @@ export class InstanceFormComponent implements OnInit { ngOnInit() { if (this.instance) { this.form.patchValue(this.instance); + this.form.controls.name.disable(); if (this.searchFormGroup.controls.search_multiple_allowed.value) { + this.searchFormGroup.controls.search_multiple_label.enable(); this.searchFormGroup.controls.search_multiple_all_datasets_selected.enable(); } + if (this.searchFormGroup.controls.search_by_criteria_allowed.value) { + this.searchFormGroup.controls.search_by_criteria_label.enable(); + } + if (this.documentationFormGroup.controls.documentation_allowed.value) { + this.documentationFormGroup.controls.documentation_label.enable(); + } } } + onChangeSelectFile(path: string) { + this.loadRootDirectory.emit(this.form.controls.data_path.value + path); + } + + isDataPathEmpty() { + return this.form.controls.data_path.value == ''; + } + submit() { if (this.instance) { this.onSubmit.emit({ diff --git a/client/src/app/admin/components/instance/search-form-group.component.html b/client/src/app/admin/components/instance/search-form-group.component.html index 4caf2ca71094ea702d08103fad8bfd4b1c586c6b..1c12343c9f1747b887de2f015f3ad760a90bf962 100644 --- a/client/src/app/admin/components/instance/search-form-group.component.html +++ b/client/src/app/admin/components/instance/search-form-group.component.html @@ -1,13 +1,21 @@ <form [formGroup]="form" novalidate> <accordion-group heading="Search"> - <div class="custom-control custom-switch"> - <input class="custom-control-input" type="checkbox" id="search_by_criteria_allowed" name="search_by_criteria_allowed" formControlName="search_by_criteria_allowed"> + <div class="custom-control custom-switch mb-2"> + <input class="custom-control-input" type="checkbox" id="search_by_criteria_allowed" name="search_by_criteria_allowed" formControlName="search_by_criteria_allowed" (change)="checkDisableSearchByCriteriaAllowed()"> <label class="custom-control-label" for="search_by_criteria_allowed">Classic search allowed</label> </div> - <div class="custom-control custom-switch"> + <div class="form-group"> + <label for="search_by_criteria_label">Label</label> + <input type="text" class="form-control" id="search_by_criteria_label" formControlName="search_by_criteria_label"> + </div> + <div class="custom-control custom-switch mb-2"> <input class="custom-control-input" type="checkbox" id="search_multiple_allowed" name="search_multiple_allowed" formControlName="search_multiple_allowed" (change)="checkDisableAllDatasetsSelected()"> <label class="custom-control-label" for="search_multiple_allowed">Search multiple allowed</label> </div> + <div class="form-group"> + <label for="search_multiple_label">Label</label> + <input type="text" class="form-control" id="search_multiple_label" formControlName="search_multiple_label"> + </div> <div class="custom-control custom-switch"> <input class="custom-control-input" type="checkbox" diff --git a/client/src/app/admin/components/instance/search-form-group.component.ts b/client/src/app/admin/components/instance/search-form-group.component.ts index 3751aa37e05eab0c2bc390501882e1e88940e59b..4a96c1bfed521b6097ff19b9ee355bde36f22cb2 100644 --- a/client/src/app/admin/components/instance/search-form-group.component.ts +++ b/client/src/app/admin/components/instance/search-form-group.component.ts @@ -17,10 +17,20 @@ import { FormGroup } from '@angular/forms'; export class SearchFormGroupComponent { @Input() form: FormGroup; + checkDisableSearchByCriteriaAllowed() { + if (this.form.controls.search_by_criteria_allowed.value) { + this.form.controls.search_by_criteria_label.enable(); + } else { + this.form.controls.search_by_criteria_label.disable(); + } + } + checkDisableAllDatasetsSelected() { if (this.form.controls.search_multiple_allowed.value) { + this.form.controls.search_multiple_label.enable(); this.form.controls.search_multiple_all_datasets_selected.enable(); } else { + this.form.controls.search_multiple_label.disable(); this.form.controls.search_multiple_all_datasets_selected.setValue(false); this.form.controls.search_multiple_all_datasets_selected.disable(); } diff --git a/client/src/app/admin/components/dataset/data-path-form-control.component.html b/client/src/app/admin/components/shared/data-path-form-control.component.html similarity index 100% rename from client/src/app/admin/components/dataset/data-path-form-control.component.html rename to client/src/app/admin/components/shared/data-path-form-control.component.html diff --git a/client/src/app/admin/components/dataset/data-path-form-control.component.ts b/client/src/app/admin/components/shared/data-path-form-control.component.ts similarity index 98% rename from client/src/app/admin/components/dataset/data-path-form-control.component.ts rename to client/src/app/admin/components/shared/data-path-form-control.component.ts index 2ca19b2342670c2b7733ae960159abde94a84328..00675df16e4ebf6163c7c1a71ab98ad6e53bfee1 100644 --- a/client/src/app/admin/components/dataset/data-path-form-control.component.ts +++ b/client/src/app/admin/components/shared/data-path-form-control.component.ts @@ -58,6 +58,7 @@ export class DataPathFormControlComponent { selectDirectory() { this.form.controls.data_path.setValue(this.fileExplorerPath); + this.form.markAsDirty(); this.modalRef.hide(); } } diff --git a/client/src/app/admin/components/shared/file-select-form-control.component.html b/client/src/app/admin/components/shared/file-select-form-control.component.html new file mode 100644 index 0000000000000000000000000000000000000000..c288526eec2f32f327f15cc0ed067c7f559720d4 --- /dev/null +++ b/client/src/app/admin/components/shared/file-select-form-control.component.html @@ -0,0 +1,60 @@ +<form [formGroup]="form" novalidate> + <div class="form-group"> + <label for="{{ controlName }}">{{ controlLabel }}</label> + <div class="input-group mb-3"> + <div class="input-group-prepend"> + <button [disabled]="disabled" (click)="openModal(template); $event.stopPropagation()" class="btn btn-outline-secondary" type="button"> + <i class="fas fa-folder-open"></i> + </button> + </div> + <input type="text" class="form-control" id="{{ controlName }}" name="{{ controlName }}" formControlName="{{ controlName }}"> + </div> + </div> +</form> + +<ng-template #template> + <div class="modal-header"> + <h4 class="modal-title pull-left">ANIS file explorer</h4> + </div> + <div> + <app-spinner *ngIf="rootDirectoryIsLoading"></app-spinner> + + <p class="ml-3 mt-3"> + <i class="far fa-folder"></i> + {{ fileExplorerPath }} + </p> + <div *ngIf="rootDirectoryIsLoaded" class="table-responsive"> + <table class="table table-hover"> + <thead> + <tr> + <th></th> + <th scope="col">Name</th> + <th scope="col">Size</th> + <th scope="col">MimeType</th> + </tr> + </thead> + <tbody> + <tr *ngFor="let fileInfo of rootDirectory" + (click)="click(fileInfo)" + [class.table-active]="checkFileSelected(fileInfo)" + [class.pointer]="fileInfo.name !== '.'"> + <td> + <span *ngIf="fileInfo.type === 'dir'"><i class="far fa-folder"></i></span> + <span *ngIf="fileInfo.type === 'file'"><i class="far fa-file"></i></span> + </td> + <td class="align-middle"> + {{ fileInfo.name }} + </td> + <td class="align-middle">{{ fileInfo.size | formatFileSize: false }}</td> + <td class="align-middle">{{ fileInfo.mimetype }}</td> + </tr> + </tbody> + </table> + </div> + </div> + <div class="modal-footer"> + <button (click)="modalRef.hide()" class="btn btn-danger">Cancel</button> + + <button [disabled]="!fileSelected" (click)="selectFile()" class="btn btn-primary">Select this file</button> + </div> +</ng-template> diff --git a/client/src/app/admin/components/shared/file-select-form-control.component.ts b/client/src/app/admin/components/shared/file-select-form-control.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..7192d97fb8f10cbcb41c1294247e93e290e3b920 --- /dev/null +++ b/client/src/app/admin/components/shared/file-select-form-control.component.ts @@ -0,0 +1,93 @@ +/** + * 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, TemplateRef, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { BsModalService } from 'ngx-bootstrap/modal'; +import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service'; + +import { FileInfo } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-file-select-form-control', + templateUrl: 'file-select-form-control.component.html' +}) +export class FileSelectFormControlComponent implements OnChanges { + @Input() form: FormGroup; + @Input() disabled: boolean = false; + @Input() controlName: string; + @Input() controlLabel: string; + @Input() rootDirectory: FileInfo[]; + @Input() rootDirectoryIsLoading: boolean; + @Input() rootDirectoryIsLoaded: boolean; + @Output() loadRootDirectory: EventEmitter<string> = new EventEmitter(); + + modalRef: BsModalRef; + fileExplorerPath = ''; + fileSelected = null; + + constructor(private modalService: BsModalService) { } + + ngOnChanges(changes: SimpleChanges) { + if (changes.disabled && changes.disabled.currentValue) { + this.form.controls[this.controlName].disable(); + } + + if (changes.disabled && !changes.disabled.currentValue) { + this.form.controls[this.controlName].enable(); + } + } + + openModal(template: TemplateRef<any>) { + const lastIndexOf = this.form.controls[this.controlName].value.lastIndexOf("/"); + this.fileExplorerPath = this.form.controls[this.controlName].value.substr(0, lastIndexOf); + if (!this.fileExplorerPath) { + this.fileExplorerPath = ''; + } + this.modalRef = this.modalService.show(template); + this.loadRootDirectory.emit(this.fileExplorerPath); + } + + click(fileInfo: FileInfo): void { + if (fileInfo.name === '.') { + return; + } + + if (fileInfo.type === 'file') { + this.fileSelected = this.buildFilePath(fileInfo); + return; + } + + if (fileInfo.name === '..') { + this.fileExplorerPath = this.fileExplorerPath.substr(0, this.fileExplorerPath.lastIndexOf("/")); + } else { + this.fileExplorerPath += '/' + fileInfo.name; + } + this.loadRootDirectory.emit(this.fileExplorerPath); + } + + buildFilePath(fileInfo: FileInfo) { + let fileSelected = ''; + if (this.fileExplorerPath !== '') { + fileSelected += this.fileExplorerPath + '/'; + } + return fileSelected + fileInfo.name; + } + + checkFileSelected(fileInfo: FileInfo) { + return this.buildFilePath(fileInfo) === this.fileSelected; + } + + selectFile() { + this.form.controls[this.controlName].setValue(this.fileSelected); + this.form.markAsDirty(); + this.modalRef.hide(); + } +} diff --git a/client/src/app/admin/components/shared/index.ts b/client/src/app/admin/components/shared/index.ts index 1e09eb0c9517cf076350a3a58196ce92e343e564..90937726cad9c4f7e83fc44b7506b46ab1ee1c6e 100644 --- a/client/src/app/admin/components/shared/index.ts +++ b/client/src/app/admin/components/shared/index.ts @@ -7,8 +7,12 @@ * file that was distributed with this source code. */ -import { DeleteBtnComponent } from "./delete-btn.component"; +import { DeleteBtnComponent } from './delete-btn.component'; +import { DataPathFormControlComponent } from './data-path-form-control.component'; +import { FileSelectFormControlComponent } from './file-select-form-control.component'; export const sharedComponents = [ - DeleteBtnComponent + DeleteBtnComponent, + DataPathFormControlComponent, + FileSelectFormControlComponent ]; diff --git a/client/src/app/admin/containers/dataset/edit-dataset.component.html b/client/src/app/admin/containers/dataset/edit-dataset.component.html index cded4c68c26e75fb31c2dbda1bb123dd46afdd3b..bc6e45c008f33fdf7a0920dd85986f68ccd97312 100644 --- a/client/src/app/admin/containers/dataset/edit-dataset.component.html +++ b/client/src/app/admin/containers/dataset/edit-dataset.component.html @@ -5,8 +5,8 @@ <a routerLink="/admin/instance-list">Instances</a> </li> <li class="breadcrumb-item active" aria-current="page"> - <a routerLink="/admin/configure-instance/{{ instanceSelected | async }}"> - Configure instance {{ instanceSelected | async }} + <a routerLink="/admin/configure-instance/{{ (instance | async).name }}"> + Configure instance {{ (instance | async).name }} </a> </li> <li class="breadcrumb-item active" aria-current="page">Edit dataset {{ datasetSelected | async }}</li> @@ -26,6 +26,7 @@ && (datasetFamilyListIsLoaded | async)" class="row"> <div class="col-12"> <app-dataset-form + [instance]="instance | async" [dataset]="datasetList | async | datasetByName:(datasetSelected | async)" [surveyList]="surveyList | async" [tableListIsLoading]="tableListIsLoading | async" @@ -44,7 +45,7 @@ <i class="fa fa-database"></i> Update dataset information </button> - <a routerLink="/admin/configure-instance/{{instanceSelected | async}}" type="button" class="btn btn-danger">Cancel</a> + <a routerLink="/admin/configure-instance/{{ (instance | async).name }}" type="button" class="btn btn-danger">Cancel</a> </app-dataset-form> </div> </div> diff --git a/client/src/app/admin/containers/dataset/edit-dataset.component.ts b/client/src/app/admin/containers/dataset/edit-dataset.component.ts index ff37287e413592da229589c438a5a7d58a02a73d..a816c37aef2efbdfbf8846c463288bee305e851e 100644 --- a/client/src/app/admin/containers/dataset/edit-dataset.component.ts +++ b/client/src/app/admin/containers/dataset/edit-dataset.component.ts @@ -13,7 +13,7 @@ import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { Store } from '@ngrx/store'; -import { Survey, DatasetFamily, Dataset, Attribute, FileInfo } from 'src/app/metamodel/models'; +import { Instance, Survey, DatasetFamily, Dataset, Attribute, FileInfo } from 'src/app/metamodel/models'; import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector'; import * as datasetActions from 'src/app/metamodel/actions/dataset.actions'; import * as attributeSelector from 'src/app/metamodel/selectors/attribute.selector'; @@ -31,7 +31,7 @@ import * as rootDirectorySelector from 'src/app/metamodel/selectors/root-directo templateUrl: 'edit-dataset.component.html' }) export class EditDatasetComponent implements OnInit { - public instanceSelected: Observable<string>; + public instance: Observable<Instance>; public datasetSelected: Observable<string>; public datasetList: Observable<Dataset[]>; public datasetListIsLoading: Observable<boolean>; @@ -53,7 +53,7 @@ export class EditDatasetComponent implements OnInit { public rootDirectoryIsLoaded: Observable<boolean>; constructor(private store: Store<{ }>, private route: ActivatedRoute) { - this.instanceSelected = store.select(instanceSelector.selectInstanceNameByRoute); + this.instance = store.select(instanceSelector.selectInstanceByRouteName); this.datasetSelected = store.select(datasetSelector.selectDatasetNameByRoute); this.datasetList = store.select(datasetSelector.selectAllDatasets); this.datasetListIsLoading = store.select(datasetSelector.selectDatasetListIsLoading); diff --git a/client/src/app/admin/containers/dataset/new-dataset.component.html b/client/src/app/admin/containers/dataset/new-dataset.component.html index 9d43f2a4fcb8b824554fe1e975d14180c31f27de..ee7e59deb9b1429ce8ca8e11b1caa2f5bc74f5c5 100644 --- a/client/src/app/admin/containers/dataset/new-dataset.component.html +++ b/client/src/app/admin/containers/dataset/new-dataset.component.html @@ -5,8 +5,8 @@ <a routerLink="/admin/instance-list">Instances</a> </li> <li class="breadcrumb-item active" aria-current="page"> - <a routerLink="/admin/configure-instance/{{ instanceSelected | async }}"> - Configure instance {{ instanceSelected | async }} + <a routerLink="/admin/configure-instance/{{ (instance | async).name }}"> + Configure instance {{ (instance | async).name }} </a> </li> <li class="breadcrumb-item active" aria-current="page">New dataset</li> @@ -20,6 +20,7 @@ <div *ngIf="(surveyListIsLoaded | async) && (datasetFamilyListIsLoaded | async)" class="row"> <div class="col-12"> <app-dataset-form + [instance]="instance | async" [surveyList]="surveyList | async" [tableListIsLoading]="tableListIsLoading | async" [tableListIsLoaded]="tableListIsLoaded | async" @@ -37,7 +38,7 @@ <i class="fa fa-database"></i> Add new dataset </button> - <a routerLink="/admin/configure-instance/{{instanceSelected | async}}" type="button" class="btn btn-danger">Cancel</a> + <a routerLink="/admin/configure-instance/{{ (instance | async).name }}" type="button" class="btn btn-danger">Cancel</a> </app-dataset-form> </div> </div> diff --git a/client/src/app/admin/containers/dataset/new-dataset.component.ts b/client/src/app/admin/containers/dataset/new-dataset.component.ts index 3bf1f9f9d848111b204887eb85ddd094c2e8ac5e..a9de9b2f2c37f518eefce4740a8b8117782f75d8 100644 --- a/client/src/app/admin/containers/dataset/new-dataset.component.ts +++ b/client/src/app/admin/containers/dataset/new-dataset.component.ts @@ -14,7 +14,7 @@ import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { Store } from '@ngrx/store'; -import { Survey, DatasetFamily, Dataset, FileInfo } from 'src/app/metamodel/models'; +import { Instance, Survey, DatasetFamily, Dataset, FileInfo } from 'src/app/metamodel/models'; import * as surveySelector from 'src/app/metamodel/selectors/survey.selector'; import * as tableActions from 'src/app/metamodel/actions/table.actions'; import * as tableSelector from 'src/app/metamodel/selectors/table.selector'; @@ -29,7 +29,7 @@ import * as rootDirectorySelector from 'src/app/metamodel/selectors/root-directo templateUrl: 'new-dataset.component.html' }) export class NewDatasetComponent implements OnInit { - public instanceSelected: Observable<string>; + public instance: Observable<Instance>; public surveyListIsLoading: Observable<boolean>; public surveyListIsLoaded: Observable<boolean>; public surveyList: Observable<Survey[]>; @@ -45,7 +45,7 @@ export class NewDatasetComponent implements OnInit { public rootDirectoryIsLoaded: Observable<boolean>; constructor(private store: Store<{ }>, private route: ActivatedRoute) { - this.instanceSelected = store.select(instanceSelector.selectInstanceNameByRoute); + this.instance = store.select(instanceSelector.selectInstanceByRouteName); this.surveyListIsLoading = store.select(surveySelector.selectSurveyListIsLoading); this.surveyListIsLoaded = store.select(surveySelector.selectSurveyListIsLoaded); this.surveyList = store.select(surveySelector.selectAllSurveys); diff --git a/client/src/app/admin/containers/group/edit-group.component.html b/client/src/app/admin/containers/group/edit-group.component.html index b568405ac87aed23f56fb2fce53ba8d7dcd0ed28..ef2c6413537669c3cd83a10c655598d4b639cb8c 100644 --- a/client/src/app/admin/containers/group/edit-group.component.html +++ b/client/src/app/admin/containers/group/edit-group.component.html @@ -2,17 +2,17 @@ <nav aria-label="breadcrumb"> <ol class="breadcrumb"> <li class="breadcrumb-item"> - <a routerLink="/instance-list"> + <a routerLink="/admin/instance-list"> Instances </a> </li> <li class="breadcrumb-item"> - <a routerLink="/configure-instance/{{ instanceName | async }}"> + <a routerLink="/admin//configure-instance/{{ instanceName | async }}"> Configure instance {{ instanceName | async }} </a> </li> <li class="breadcrumb-item" aria-current="page"> - <a routerLink="/configure-instance/{{ instanceName | async }}/group"> + <a routerLink="/admin/configure-instance/{{ instanceName | async }}/group"> Groups </a> </li> diff --git a/client/src/app/admin/containers/instance/edit-instance.component.html b/client/src/app/admin/containers/instance/edit-instance.component.html index 8db55ccca330f28e87bf795e46e36488f6adeeaa..cdd51e099a38c6ae296a0598b5d7691bcbc654da 100644 --- a/client/src/app/admin/containers/instance/edit-instance.component.html +++ b/client/src/app/admin/containers/instance/edit-instance.component.html @@ -10,7 +10,14 @@ <div class="container"> <div class="row"> <div class="col-12"> - <app-instance-form [instance]="instance | async" (onSubmit)="editInstance($event)" #formInstance> + <app-instance-form + [instance]="instance | async" + [rootDirectory]="rootDirectory | async" + [rootDirectoryIsLoading]="rootDirectoryIsLoading | async" + [rootDirectoryIsLoaded]="rootDirectoryIsLoaded | async" + (loadRootDirectory)="loadRootDirectory($event)" + (onSubmit)="editInstance($event)" + #formInstance> <button [disabled]="!formInstance.form.valid || formInstance.form.pristine" type="submit" class="btn btn-primary"> <span class="fa fa-database"></span> Update instance information </button> diff --git a/client/src/app/admin/containers/instance/edit-instance.component.ts b/client/src/app/admin/containers/instance/edit-instance.component.ts index e26d6aa6da8962e82f56f01b61f1548cf27d7676..278e28cb6c2d8d583151cbf1373708acb58b937b 100644 --- a/client/src/app/admin/containers/instance/edit-instance.component.ts +++ b/client/src/app/admin/containers/instance/edit-instance.component.ts @@ -12,9 +12,11 @@ import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { Instance } from 'src/app/metamodel/models'; +import { Instance, FileInfo } from 'src/app/metamodel/models'; import * as instanceActions from 'src/app/metamodel/actions/instance.actions'; import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; +import * as rootDirectoryActions from 'src/app/metamodel/actions/root-directory.actions'; +import * as rootDirectorySelector from 'src/app/metamodel/selectors/root-directory.selector'; @Component({ selector: 'app-edit-instance', @@ -22,12 +24,22 @@ import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector }) export class EditInstanceComponent { public instance: Observable<Instance>; + public rootDirectory: Observable<FileInfo[]>; + public rootDirectoryIsLoading: Observable<boolean>; + public rootDirectoryIsLoaded: Observable<boolean>; constructor(private store: Store<{ }>) { this.instance = store.select(instanceSelector.selectInstanceByRouteName); + this.rootDirectory = store.select(rootDirectorySelector.selectAllFileInfo); + this.rootDirectoryIsLoading = store.select(rootDirectorySelector.selectRootDirectoryIsLoading); + this.rootDirectoryIsLoaded = store.select(rootDirectorySelector.selectRootDirectoryIsLoaded); } editInstance(instance: Instance) { this.store.dispatch(instanceActions.editInstance({ instance })); } + + loadRootDirectory(path: string) { + this.store.dispatch(rootDirectoryActions.loadRootDirectory({ path })); + } } diff --git a/client/src/app/admin/containers/instance/new-instance.component.html b/client/src/app/admin/containers/instance/new-instance.component.html index 3025b2d9246cf1ca41de390cb7df4a610a0a7c84..58d637cd740c2a2e9a1e67cefdc99ee0dab098c1 100644 --- a/client/src/app/admin/containers/instance/new-instance.component.html +++ b/client/src/app/admin/containers/instance/new-instance.component.html @@ -10,7 +10,13 @@ <div class="container"> <div class="row"> <div class="col-12"> - <app-instance-form (onSubmit)="addNewInstance($event)" #formInstance> + <app-instance-form + [rootDirectory]="rootDirectory | async" + [rootDirectoryIsLoading]="rootDirectoryIsLoading | async" + [rootDirectoryIsLoaded]="rootDirectoryIsLoaded | async" + (loadRootDirectory)="loadRootDirectory($event)" + (onSubmit)="addNewInstance($event)" + #formInstance> <button [disabled]="!formInstance.form.valid || formInstance.form.pristine" type="submit" class="btn btn-primary"> <span class="fa fa-database"></span> Add the new instance </button> diff --git a/client/src/app/admin/containers/instance/new-instance.component.ts b/client/src/app/admin/containers/instance/new-instance.component.ts index abe0ab1936d78603e17a52fba962678110bc2b34..9c3383cf43af6ed532228bf34897d3d1b3d57333 100644 --- a/client/src/app/admin/containers/instance/new-instance.component.ts +++ b/client/src/app/admin/containers/instance/new-instance.component.ts @@ -10,17 +10,33 @@ import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Instance } from 'src/app/metamodel/models'; -import * as instanceActions from 'src/app/metamodel/actions/instance.actions' +import { Observable } from 'rxjs'; + +import { Instance, FileInfo } from 'src/app/metamodel/models'; +import * as instanceActions from 'src/app/metamodel/actions/instance.actions'; +import * as rootDirectoryActions from 'src/app/metamodel/actions/root-directory.actions'; +import * as rootDirectorySelector from 'src/app/metamodel/selectors/root-directory.selector'; @Component({ selector: 'app-new-instance', templateUrl: 'new-instance.component.html' }) export class NewInstanceComponent { - constructor(private store: Store<{ }>) { } + public rootDirectory: Observable<FileInfo[]>; + public rootDirectoryIsLoading: Observable<boolean>; + public rootDirectoryIsLoaded: Observable<boolean>; + + constructor(private store: Store<{ }>) { + this.rootDirectory = store.select(rootDirectorySelector.selectAllFileInfo); + this.rootDirectoryIsLoading = store.select(rootDirectorySelector.selectRootDirectoryIsLoading); + this.rootDirectoryIsLoaded = store.select(rootDirectorySelector.selectRootDirectoryIsLoaded); + } addNewInstance(instance: Instance) { this.store.dispatch(instanceActions.addInstance({ instance })); } + + loadRootDirectory(path: string) { + this.store.dispatch(rootDirectoryActions.loadRootDirectory({ path })); + } } diff --git a/client/src/app/app-init.ts b/client/src/app/app-init.ts index b3dc042681bc40d7d903a4e8585e2349fdff7613..6b9241ee9afd93090d03991dda756a79062137d4 100644 --- a/client/src/app/app-init.ts +++ b/client/src/app/app-init.ts @@ -7,12 +7,15 @@ import { Store } from '@ngrx/store'; import { AppConfigService } from './app-config.service'; import { initializeKeycloak } from 'src/app/auth/init.keycloak'; +import { environment } from 'src/environments/environment'; + function appInit(http: HttpClient, appConfigService: AppConfigService, keycloak: KeycloakService, store: Store<{ }>) { return () => { - return http.get('/assets/app.config.json') + return http.get(`${environment.apiUrl}/client-settings`) .toPromise() .then(data => { Object.assign(appConfigService, data); + appConfigService.apiUrl = environment.apiUrl; return appConfigService; }) .then(appConfigService => { diff --git a/client/src/app/app.reducer.ts b/client/src/app/app.reducer.ts index d35a1319b646cc92f2b6057c6d6f54a421ffff2f..26c5ea0d66124663115b3bbbe53feb956c53fa28 100644 --- a/client/src/app/app.reducer.ts +++ b/client/src/app/app.reducer.ts @@ -10,6 +10,8 @@ import { ActionReducerMap, ActionReducer, MetaReducer } from '@ngrx/store'; import { RouterReducerState, routerReducer } from '@ngrx/router-store'; +import { environment } from 'src/environments/environment'; + export interface State { router: RouterReducerState; } @@ -31,5 +33,7 @@ export function debug(reducer: ActionReducer<any>): ActionReducer<any> { return nextState; }; } - -export const metaReducers: MetaReducer<any>[] = [debug]; + +export const metaReducers: MetaReducer<any>[] = !environment.production + ? [debug] + : []; diff --git a/client/src/app/auth/auth.actions.ts b/client/src/app/auth/auth.actions.ts index 52da0604574a5b963966ee9e99312cf229c29d91..429574a4716d4c1db8670e584eed302f148fa37f 100644 --- a/client/src/app/auth/auth.actions.ts +++ b/client/src/app/auth/auth.actions.ts @@ -15,5 +15,5 @@ export const login = createAction('[Auth] Login'); export const logout = createAction('[Auth] Logout'); export const authSuccess = createAction('[Auth] Auth Success'); export const loadUserProfileSuccess = createAction('[Auth] Load User Profile Success', props<{ userProfile: UserProfile }>()); -export const loadUserRoleSuccess = createAction('[Auth] Load User Roles Success', props<{ userRoles: string[] }>()); +export const loadUserRolesSuccess = createAction('[Auth] Load User Roles Success', props<{ userRoles: string[] }>()); export const openEditProfile = createAction('[Auth] Edit Profile'); diff --git a/client/src/app/auth/auth.effects.ts b/client/src/app/auth/auth.effects.ts index 69cefb0e2b4f468564a02cdcbafb631a22113e7f..bdb76a950fa0644e54dba3b9add95f7061e884bd 100644 --- a/client/src/app/auth/auth.effects.ts +++ b/client/src/app/auth/auth.effects.ts @@ -10,7 +10,7 @@ import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { from } from 'rxjs'; -import { tap, switchMap } from 'rxjs/operators'; +import { tap, switchMap, withLatestFrom } from 'rxjs/operators'; import { KeycloakService } from 'keycloak-angular'; @@ -51,7 +51,7 @@ export class AuthEffects { .pipe( switchMap(userProfile => [ authActions.loadUserProfileSuccess({ userProfile }), - authActions.loadUserRoleSuccess({ userRoles: this.keycloak.getUserRoles() }) + authActions.loadUserRolesSuccess({ userRoles: this.keycloak.getUserRoles() }), ]) ) ) diff --git a/client/src/app/auth/auth.reducer.ts b/client/src/app/auth/auth.reducer.ts index d0805affc1d7dfeb02d4b6f6c2cc2bf728c401c5..a516f9c9c9056ed0f26ded6a598f88a91a7f198b 100644 --- a/client/src/app/auth/auth.reducer.ts +++ b/client/src/app/auth/auth.reducer.ts @@ -34,7 +34,7 @@ export const authReducer = createReducer( ...state, userProfile })), - on(authActions.loadUserRoleSuccess, (state, { userRoles }) => ({ + on(authActions.loadUserRolesSuccess, (state, { userRoles }) => ({ ...state, userRoles })) diff --git a/client/src/app/auth/init.keycloak.ts b/client/src/app/auth/init.keycloak.ts index a5186253c05dd947ef3c8518fce6652ea60ced12..34ee8d9f7490c1d5f046dd676996fa9d50e97894 100644 --- a/client/src/app/auth/init.keycloak.ts +++ b/client/src/app/auth/init.keycloak.ts @@ -21,9 +21,15 @@ export function initializeKeycloak(keycloak: KeycloakService, store: Store<{ }>, } from(keycloak.keycloakEvents$).subscribe(event => { + if (event.type === KeycloakEventType.OnAuthLogout) { + store.dispatch(keycloakActions.logout()); + } if (event.type === KeycloakEventType.OnAuthSuccess) { store.dispatch(keycloakActions.authSuccess()); } + if (event.type === KeycloakEventType.OnAuthRefreshError) { + store.dispatch(keycloakActions.login()); + } }) let silentCheckSsoRedirectUri = window.location.origin; @@ -42,6 +48,7 @@ export function initializeKeycloak(keycloak: KeycloakService, store: Store<{ }>, onLoad: 'check-sso', silentCheckSsoRedirectUri }, - loadUserProfileAtStartUp: true + loadUserProfileAtStartUp: true, + bearerExcludedUrls: ['https://cdsweb.u-strasbg.fr/'] }); } diff --git a/client/src/app/instance/documentation/components/dataset-by-family.component.spec.ts b/client/src/app/instance/documentation/components/dataset-by-family.component.spec.ts index 823eca11f37fc14bef55b5be2ba7208ef9e84840..0f2d8314abde7e7dcfbed4c1bb3c53ddc171ea32 100644 --- a/client/src/app/instance/documentation/components/dataset-by-family.component.spec.ts +++ b/client/src/app/instance/documentation/components/dataset-by-family.component.spec.ts @@ -18,8 +18,13 @@ const DATASET_LIST: Dataset[] = [ survey_name: 'mySurvey', id_dataset_family: 1, public: true, + full_data_path: '/data/path1', config: { images: ['image1'], + survey: { + survey_enabled: true, + survey_label: 'More about this survey' + }, cone_search: { cone_search_enabled: true, cone_search_opened: true, @@ -67,8 +72,13 @@ const DATASET_LIST: Dataset[] = [ survey_name: 'mySurvey', id_dataset_family: 1, public: true, + full_data_path: '/data/path2', config: { images: ['image1'], + survey: { + survey_enabled: true, + survey_label: 'More about this survey' + }, cone_search: { cone_search_enabled: true, cone_search_opened: true, diff --git a/client/src/app/instance/documentation/components/dataset-card-doc.component.spec.ts b/client/src/app/instance/documentation/components/dataset-card-doc.component.spec.ts index 7911b704f2e874d514ddb9d1873f003ac379160a..0140dddacee3b1f1aa33a8903f29bdd97cc2553b 100644 --- a/client/src/app/instance/documentation/components/dataset-card-doc.component.spec.ts +++ b/client/src/app/instance/documentation/components/dataset-card-doc.component.spec.ts @@ -21,12 +21,17 @@ const DATASET: Dataset = { label: 'my dataset', description: 'This is my dataset', display: 1, - data_path: 'path', + data_path: '/path', survey_name: 'mySurvey', id_dataset_family: 1, public: true, + full_data_path: '/data/path', config: { images: ['image1'], + survey: { + survey_enabled: true, + survey_label: 'More about this survey' + }, cone_search: { cone_search_enabled: true, cone_search_opened: true, diff --git a/client/src/app/instance/home/components/index.ts b/client/src/app/instance/home/components/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8973b374e7bfb09e0ded7e6230d0170a6e266273 --- /dev/null +++ b/client/src/app/instance/home/components/index.ts @@ -0,0 +1,5 @@ +import { WelcomeComponent } from './welcome.component'; + +export const dummiesComponents = [ + WelcomeComponent +]; diff --git a/client/src/app/instance/home/components/welcome.component.html b/client/src/app/instance/home/components/welcome.component.html new file mode 100644 index 0000000000000000000000000000000000000000..0a1998a5e06ccdbaad4f020318d91cef1380d552 --- /dev/null +++ b/client/src/app/instance/home/components/welcome.component.html @@ -0,0 +1,6 @@ +<div class="row align-items-center jumbotron"> + <div class="col-6 col-md-4 order-md-2 mx-auto text-center"> + <img class="img-fluid mb-3 mb-md-0" src="{{ getLogoSrc() }}" alt=""> + </div> + <div class="col-md-8 order-md-1 text-justify pr-md-5" [innerHtml]="instance.config.home.home_config.home_component_text"></div> +</div> \ No newline at end of file diff --git a/client/src/app/instance/home/home.component.scss b/client/src/app/instance/home/components/welcome.component.scss similarity index 80% rename from client/src/app/instance/home/home.component.scss rename to client/src/app/instance/home/components/welcome.component.scss index ec44a92b7518f17d66bcacdaab53031f36bdc18b..0a76fe2eca34d1a78330bf489fa2c6547fb46071 100644 --- a/client/src/app/instance/home/home.component.scss +++ b/client/src/app/instance/home/components/welcome.component.scss @@ -7,11 +7,6 @@ * file that was distributed with this source code. */ - div.jumbotron h1 { - font-weight: bold; - letter-spacing: 8px; -} - div.jumbotron p { line-height: 35px; } diff --git a/client/src/app/instance/home/components/welcome.component.ts b/client/src/app/instance/home/components/welcome.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..96095b046138fb3951c76efa333f8e50b0a29f4e --- /dev/null +++ b/client/src/app/instance/home/components/welcome.component.ts @@ -0,0 +1,30 @@ +/** + * 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 { Instance } from 'src/app/metamodel/models'; + +/** + * @class + * @classdesc Home container. + */ +@Component({ + selector: 'app-welcome', + styleUrls: ['welcome.component.scss'], + templateUrl: 'welcome.component.html' +}) +export class WelcomeComponent { + @Input() instance: Instance; + @Input() apiUrl: string; + + getLogoSrc() { + return `${this.apiUrl}/download-instance-file/${this.instance.name}/${this.instance.config.home.home_config.home_component_logo}`; + } +} diff --git a/client/src/app/instance/home/home.component.html b/client/src/app/instance/home/home.component.html index 9454b1e1567473c2f52c18b0db098e6087065d30..96b9457fbe7b3a811f4c17a53931bf9fe3ab32ce 100644 --- a/client/src/app/instance/home/home.component.html +++ b/client/src/app/instance/home/home.component.html @@ -1,20 +1,3 @@ <div class="container"> - <div class="row align-items-center jumbotron"> - <div class="col-6 col-md-4 order-md-2 mx-auto text-center"> - <img class="img-fluid mb-3 mb-md-0" src="assets/anis_v3_logo300.png" alt=""> - </div> - <div class="col-md-8 order-md-1 text-justify pr-md-5"> - <h1 class="mb-3">ANIS</h1> - <p class="lead"> - AstroNomical Information System is a generic web tool aimed at facilitating and homogenizing the - implementation of astronomical data. - It allows the fast implementation of a project data exchange platform in a dedicated information - system. - </p> - <p class="lead"> - ANIS provides services like searching, displaying images and spectroscopic data and - downloading catalogues. - </p> - </div> - </div> + <app-welcome [instance]="instance | async" [apiUrl]="getApiUrl()"></app-welcome> </div> diff --git a/client/src/app/instance/home/home.component.ts b/client/src/app/instance/home/home.component.ts index 7182ca06f7a64c23d9dcf467147e20347119baa0..e120190feec76ac65ffa83c733ea33a83c327a4a 100644 --- a/client/src/app/instance/home/home.component.ts +++ b/client/src/app/instance/home/home.component.ts @@ -9,13 +9,29 @@ import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; + +import { Instance } from 'src/app/metamodel/models'; +import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; +import { AppConfigService } from 'src/app/app-config.service'; + /** * @class * @classdesc Home container. */ @Component({ selector: 'app-home', - styleUrls: ['home.component.scss'], templateUrl: 'home.component.html' }) -export class HomeComponent { } +export class HomeComponent { + public instance: Observable<Instance>; + + constructor(private store: Store<{ }>, private config: AppConfigService) { + this.instance = this.store.select(instanceSelector.selectInstanceByRouteName); + } + + getApiUrl() { + return this.config.apiUrl; + } +} diff --git a/client/src/app/instance/home/home.module.ts b/client/src/app/instance/home/home.module.ts index 96c4a89bef72e24cb60e9b1d8df619cce10867e7..0859f9d1d45bb11bc68c64553fafe1b5961d8ef3 100644 --- a/client/src/app/instance/home/home.module.ts +++ b/client/src/app/instance/home/home.module.ts @@ -11,6 +11,7 @@ import { NgModule } from '@angular/core'; import { SharedModule } from 'src/app/shared/shared.module'; import { HomeRoutingModule, routedComponents } from './home-routing.module'; +import { dummiesComponents } from './components'; /** * @class @@ -21,6 +22,9 @@ import { HomeRoutingModule, routedComponents } from './home-routing.module'; SharedModule, HomeRoutingModule ], - declarations: [routedComponents] + declarations: [ + routedComponents, + dummiesComponents + ] }) export class HomeModule { } diff --git a/client/src/app/instance/instance-routing.module.ts b/client/src/app/instance/instance-routing.module.ts index 03c0e21f5b879295d917d7152162fc4e94e73988..53ae1296d4a2648f295956697e42626368d31be5 100644 --- a/client/src/app/instance/instance-routing.module.ts +++ b/client/src/app/instance/instance-routing.module.ts @@ -18,6 +18,7 @@ const routes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', loadChildren: () => import('./home/home.module').then(m => m.HomeModule) }, { path: 'search', loadChildren: () => import('./search/search.module').then(m => m.SearchModule) }, + { path: 'search-multiple', loadChildren: () => import('./search-multiple/search-multiple.module').then(m => m.SearchMultipleModule) }, { path: 'documentation', loadChildren: () => import('./documentation/documentation.module').then(m => m.DocumentationModule) } ] } diff --git a/client/src/app/instance/instance.component.html b/client/src/app/instance/instance.component.html index acd9b3df900a6771327d87a05dd6b10e734800fb..ef03a4e2a31e4b464ad0ca7c12ac0d95aa5ea0c8 100644 --- a/client/src/app/instance/instance.component.html +++ b/client/src/app/instance/instance.component.html @@ -5,6 +5,7 @@ [userProfile]="userProfile | async" [baseHref]="getBaseHref()" [authenticationEnabled]="getAuthenticationEnabled()" + [apiUrl]="getApiUrl()" [instance]="instance | async" (login)="login()" (logout)="logout()" diff --git a/client/src/app/instance/instance.component.ts b/client/src/app/instance/instance.component.ts index b433bd32845d2904c6a6efe0e5c67c60ad4090bb..fa981f4b2c09c1d62b2d27f7ee1c1917a6ca8db7 100644 --- a/client/src/app/instance/instance.component.ts +++ b/client/src/app/instance/instance.component.ts @@ -31,6 +31,7 @@ import { AppConfigService } from 'src/app/app-config.service'; */ export class InstanceComponent implements OnInit, OnDestroy { public favIcon: HTMLLinkElement = document.querySelector('#favicon'); + public title: HTMLLinkElement = document.querySelector('#title'); public links = [ { label: 'Home', icon: 'fas fa-home', routerLink: 'home' } ]; @@ -54,15 +55,18 @@ export class InstanceComponent implements OnInit, OnDestroy { Promise.resolve(null).then(() => this.store.dispatch(surveyActions.loadSurveyList())); this.instanceSubscription = this.instance.subscribe(instance => { if (instance.config.search.search_by_criteria_allowed) { - this.links.push({ label: 'Search', icon: 'fas fa-search', routerLink: 'search' }); + this.links.push({ label: instance.config.search.search_by_criteria_label, icon: 'fas fa-search', routerLink: 'search' }); } if (instance.config.search.search_multiple_allowed) { - this.links.push({ label: 'Search multiple', icon: 'fas fa-search-plus', routerLink: 'search-multiple' }); + this.links.push({ label: instance.config.search.search_multiple_label, icon: 'fas fa-search-plus', routerLink: 'search-multiple' }); } if (instance.config.documentation.documentation_allowed) { - this.links.push({ label: 'Documentation', icon: 'fas fa-question', routerLink: 'documentation' }); + this.links.push({ label: instance.config.documentation.documentation_label, icon: 'fas fa-question', routerLink: 'documentation' }); } - this.favIcon.href = `assets/${instance.name}-favicon.ico`; + if (instance.config.design.design_favicon !== '') { + this.favIcon.href = `${this.config.apiUrl}/download-instance-file/${instance.name}/${instance.config.design.design_favicon}`; + } + this.title.innerHTML = instance.label; }) } @@ -74,6 +78,10 @@ export class InstanceComponent implements OnInit, OnDestroy { return this.config.authenticationEnabled; } + getApiUrl() { + return this.config.apiUrl; + } + login(): void { this.store.dispatch(authActions.login()); } diff --git a/client/src/app/instance/instance.reducer.ts b/client/src/app/instance/instance.reducer.ts index 76e651beb8b286a870569e434a9c274b4ba1369a..d14c08ae52efefef8b043088ae0b74cddbb1a44d 100644 --- a/client/src/app/instance/instance.reducer.ts +++ b/client/src/app/instance/instance.reducer.ts @@ -11,12 +11,14 @@ import { combineReducers, createFeatureSelector } from '@ngrx/store'; import { RouterReducerState } from 'src/app/custom-route-serializer'; import * as search from './store/reducers/search.reducer'; +import * as searchMultiple from './store/reducers/search-multiple.reducer'; import * as samp from './store/reducers/samp.reducer'; import * as coneSearch from './store/reducers/cone-search.reducer'; import * as detail from './store/reducers/detail.reducer'; export interface State { search: search.State, + searchMultiple: searchMultiple.State, samp: samp.State, coneSearch: coneSearch.State detail: detail.State @@ -24,6 +26,7 @@ export interface State { const reducers = { search: search.searchReducer, + searchMultiple: searchMultiple.searchMultipleReducer, samp: samp.sampReducer, coneSearch: coneSearch.coneSearchReducer, detail: detail.detailReducer diff --git a/client/src/app/instance/search-multiple/components/datasets/dataset-list.component.html b/client/src/app/instance/search-multiple/components/datasets/dataset-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..4395a45c9d7adad1d8ebe8fa6598b3c3f90f6fee --- /dev/null +++ b/client/src/app/instance/search-multiple/components/datasets/dataset-list.component.html @@ -0,0 +1,13 @@ +<div class="row"> + <div *ngFor="let datasetFamily of getDatasetFamilyList()" class="col-12 col-lg-6 col-xl-4 my-3 text-center"> + <app-datasets-by-family + [datasetFamily]="datasetFamily" + [datasetList]="getDatasetsByFamily(datasetFamily.id)" + [surveyList]="surveyList" + [selectedDatasets]="selectedDatasets" + [isAllSelected]="getIsAllSelected(datasetFamily.id)" + [isAllUnselected]="getIsAllUnselected(datasetFamily.id)" + (updateSelectedDatasets)="updateSelectedDatasets.emit($event)"> + </app-datasets-by-family> + </div> +</div> diff --git a/client/src/app/instance/search-multiple/components/datasets/dataset-list.component.ts b/client/src/app/instance/search-multiple/components/datasets/dataset-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f01569abfd679097f564b04a1563c52806e6957e --- /dev/null +++ b/client/src/app/instance/search-multiple/components/datasets/dataset-list.component.ts @@ -0,0 +1,82 @@ +/** + * 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, ChangeDetectionStrategy } from '@angular/core'; + +import { Dataset, DatasetFamily, Survey } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-dataset-list', + templateUrl: 'dataset-list.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +/** + * @class + * @classdesc Search multiple dataset list component. + */ +export class DatasetListComponent { + @Input() datasetFamilyList: DatasetFamily[]; + @Input() datasetList: Dataset[]; + @Input() surveyList: Survey[]; + @Input() selectedDatasets: string[]; + @Output() updateSelectedDatasets: EventEmitter<string[]> = new EventEmitter(); + + /** + * Returns dataset family list sorted by display, that contains datasets with cone search enabled. + * + * @return Family[] + */ + getDatasetFamilyList(): DatasetFamily[] { + const familyId: number[] = []; + this.datasetList.forEach(d => { + if (!familyId.includes(d.id_dataset_family)) { + familyId.push(d.id_dataset_family); + } + }); + return this.datasetFamilyList + .filter(f => familyId.includes(f.id)); + } + + /** + * Returns dataset list that belongs to the given ID family. + * + * @param {number} familyId - The dataset family ID. + * + * @return Dataset[] + */ + getDatasetsByFamily(familyId: number): Dataset[] { + return this.datasetList.filter(d => d.id_dataset_family === familyId); + } + + /** + * Checks if all datasets that belongs to the given dataset family ID are selected. + * + * @param {number} familyId - The dataset family ID. + * + * @return boolean + */ + getIsAllSelected(familyId: number): boolean { + const datasetListName = this.getDatasetsByFamily(familyId).map(d => d.name); + const filteredSelectedDatasets = this.selectedDatasets.filter(name => datasetListName.indexOf(name) > -1); + return datasetListName.length === filteredSelectedDatasets.length; + } + + /** + * Checks if none of datasets that belongs to the given dataset family ID are selected. + * + * @param {number} familyId - The dataset family ID. + * + * @return boolean + */ + getIsAllUnselected(familyId: number): boolean { + const datasetListName = this.getDatasetsByFamily(familyId).map(d => d.name); + const filteredSelectedDatasets = this.selectedDatasets.filter(name => datasetListName.indexOf(name) > -1); + return filteredSelectedDatasets.length === 0; + } +} diff --git a/client/src/app/instance/search-multiple/components/datasets/datasets-by-family.component.html b/client/src/app/instance/search-multiple/components/datasets/datasets-by-family.component.html new file mode 100644 index 0000000000000000000000000000000000000000..c0ab965e6bdb4d3cabde7446155f1f95ae62490d --- /dev/null +++ b/client/src/app/instance/search-multiple/components/datasets/datasets-by-family.component.html @@ -0,0 +1,43 @@ +<p class="mb-3"><em>{{ datasetFamily.label }}</em></p> +<div class="row mb-1"> + <div class="col pr-1"> + <button (click)="selectAll()" [disabled]="isAllSelected" + class="btn btn-sm btn-block btn-outline-secondary letter-spacing"> + Select All + </button> + </div> + <div class="col pl-1"> + <button (click)="unselectAll()" [disabled]="isAllUnselected" + class="btn btn-sm btn-block btn-outline-secondary letter-spacing"> + Unselect All + </button> + </div> +</div> +<div class="selectbox p-0"> + <div *ngFor="let dataset of datasetList"> + <div *ngIf="isSelected(dataset.name)"> + <button class="btn btn-block text-left py-1 m-0 rounded-0" (click)="toggleSelection(dataset.name)"> + <span class="fas fa-fw fa-check-square theme-color"></span> + {{ dataset.label }} + <span [tooltip]="datasetInfo" placement="right" containerClass="custom-tooltip right-tooltip"> + <span class="far fa-question-circle fa-xs"></span> + </span> + </button> + </div> + <div *ngIf="!isSelected(dataset.name)"> + <button class="btn btn-block text-left py-1 m-0 rounded-0" (click)="toggleSelection(dataset.name)"> + <span class="far fa-fw fa-square text-secondary"></span> + {{ dataset.label }} + <span [tooltip]="datasetInfo" placement="right" containerClass="custom-tooltip right-tooltip"> + <span class="far fa-question-circle fa-xs"></span> + </span> + </button> + </div> + + <ng-template #datasetInfo class="text-left"> + {{ dataset.description }} + <br><br> + {{ getSurveyDescription(dataset.survey_name) }} + </ng-template> + </div> +</div> diff --git a/client/src/app/instance/search-multiple/components/datasets/datasets-by-family.component.scss b/client/src/app/instance/search-multiple/components/datasets/datasets-by-family.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..14afb86a712e88c6595f1578648cb2b5c8040dbb --- /dev/null +++ b/client/src/app/instance/search-multiple/components/datasets/datasets-by-family.component.scss @@ -0,0 +1,27 @@ +/** + * 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. + */ + +.selectbox { + height: 200px; + overflow-y: auto; + border: 1px solid #ced4da; + border-radius: .25rem; +} + +.letter-spacing { + letter-spacing: 2px; +} + +.selectbox button:hover { + background-color: #ECECEC; +} + +.selectbox button:focus { + box-shadow: none; +} diff --git a/client/src/app/instance/search-multiple/components/datasets/datasets-by-family.component.ts b/client/src/app/instance/search-multiple/components/datasets/datasets-by-family.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..8a1a260c5e72f0f86b46ee6910c9e76f1bfe5976 --- /dev/null +++ b/client/src/app/instance/search-multiple/components/datasets/datasets-by-family.component.ts @@ -0,0 +1,96 @@ +/** + * 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, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core'; + +import { Dataset, DatasetFamily, Survey } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-datasets-by-family', + templateUrl: 'datasets-by-family.component.html', + styleUrls: ['datasets-by-family.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None +}) +/** + * @class + * @classdesc Search multiple datasets by family component. + */ +export class DatasetsByFamilyComponent { + @Input() datasetFamily: DatasetFamily; + @Input() datasetList: Dataset[]; + @Input() surveyList: Survey[]; + @Input() selectedDatasets: string[]; + @Input() isAllSelected: boolean; + @Input() isAllUnselected: boolean; + @Output() updateSelectedDatasets: EventEmitter<string[]> = new EventEmitter(); + + /** + * Checks if dataset is selected fot the given dataset name. + * + * @param {string} dname - The dataset name. + * + * @return boolean + */ + isSelected(dname: string): boolean { + return this.selectedDatasets.filter(i => i === dname).length > 0; + } + + /** + * Emits event to update dataset list selection with the given updated selected dataset name list. + * + * @param {string} dname - The dataset name. + */ + toggleSelection(dname: string): void { + const clonedSelectedDatasets = [...this.selectedDatasets]; + const index = clonedSelectedDatasets.indexOf(dname); + if (index > -1) { + clonedSelectedDatasets.splice(index, 1); + } else { + clonedSelectedDatasets.push(dname); + } + this.updateSelectedDatasets.emit(clonedSelectedDatasets); + } + + /** + * Emits event to update dataset list selection with all datasets names. + */ + selectAll(): void { + const clonedSelectedDatasets = [...this.selectedDatasets]; + const datasetListName = this.datasetList.map(d => d.name); + datasetListName.filter(name => clonedSelectedDatasets.indexOf(name) === -1).forEach(name => { + clonedSelectedDatasets.push(name); + }); + this.updateSelectedDatasets.emit(clonedSelectedDatasets); + } + + /** + * Emits event to update dataset list selection with no datasets names. + */ + unselectAll(): void { + const clonedSelectedDatasets = [...this.selectedDatasets]; + const datasetListName = this.datasetList.map(d => d.name); + datasetListName.filter(name => clonedSelectedDatasets.indexOf(name) > -1).forEach(name => { + const index = clonedSelectedDatasets.indexOf(name); + clonedSelectedDatasets.splice(index, 1); + }); + this.updateSelectedDatasets.emit(clonedSelectedDatasets); + } + + /** + * Returns survey description of the given survey name. + * + * @param {string} surveyName - The survey name. + * + * @return string + */ + getSurveyDescription(surveyName: string): string { + return this.surveyList.find(p => p.name === surveyName).description; + } +} diff --git a/client/src/app/instance/search-multiple/components/datasets/index.ts b/client/src/app/instance/search-multiple/components/datasets/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..730e87c6c191b0313c5f71920538f3374fe5245f --- /dev/null +++ b/client/src/app/instance/search-multiple/components/datasets/index.ts @@ -0,0 +1,7 @@ +import { DatasetListComponent } from './dataset-list.component'; +import { DatasetsByFamilyComponent } from './datasets-by-family.component'; + +export const datasetsComponents = [ + DatasetListComponent, + DatasetsByFamilyComponent +]; diff --git a/client/src/app/instance/search-multiple/components/index.ts b/client/src/app/instance/search-multiple/components/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..53355e402864869e82505c5ac93da6e6c7da61ec --- /dev/null +++ b/client/src/app/instance/search-multiple/components/index.ts @@ -0,0 +1,11 @@ +import { ProgressBarMultipleComponent } from './progress-bar-multiple.component'; +import { SummaryMultipleComponent } from './summary-multiple.component'; +import { datasetsComponents } from './datasets'; +import { resultComponents } from './result'; + +export const dummiesComponents = [ + ProgressBarMultipleComponent, + SummaryMultipleComponent, + datasetsComponents, + resultComponents +]; diff --git a/client/src/app/instance/search-multiple/components/progress-bar-multiple.component.html b/client/src/app/instance/search-multiple/components/progress-bar-multiple.component.html new file mode 100644 index 0000000000000000000000000000000000000000..7ac8fc5b28b1f1ba5aea2ee70bc3f431787446ef --- /dev/null +++ b/client/src/app/instance/search-multiple/components/progress-bar-multiple.component.html @@ -0,0 +1,58 @@ +<div class="row text-center"> + <div class="col"> + <h1>Search around a position in multiple datasets</h1> + <p class="text-muted">Fill RA & DEC position, select datasets and display the result.</p> + </div> +</div> +<div class="progress-navigation"> + <div class="progress progress-with-circle"> + <div class="progress-bar" + [ngClass]="getStepClass()" + [ngStyle]="{'background-color': instance.config.design.design_color }" + role="progressbar" + aria-valuenow="1" + aria-valuemin="1" + aria-valuemax="4"> + </div> + </div> + <ul class="nav nav-pills"> + <li class="nav-item checked" [ngClass]="{'active': currentStep === 'position'}"> + <a class="nav-link" [ngStyle]="getNavItemAStyle('position', true)" routerLink="/instance/{{ instance.name }}/search-multiple/position" [queryParams]="queryParams" data-toggle="tab"> + <div class="icon-circle" [ngStyle]="getNavItemIconCircleStyle('position', true)"> + <span class="fas fa-drafting-compass"></span> + </div> + Position + </a> + </li> + + <li class="nav-item" [ngClass]="{'active': currentStep === 'datasets', 'checked': datasetsStepChecked}"> + <a *ngIf="coneSearch" class="nav-link" [ngStyle]="getNavItemAStyle('datasets', datasetsStepChecked)" routerLink="/instance/{{ instance.name }}/search-multiple/datasets" [queryParams]="queryParams" data-toggle="tab"> + <div class="icon-circle" [ngStyle]="getNavItemIconCircleStyle('datasets', datasetsStepChecked)"> + <span class="fas fa-book"></span> + </div> + Datasets + </a> + <a *ngIf="!coneSearch" class="nav-link disabled" [ngStyle]="getNavItemAStyle('datasets', datasetsStepChecked)" data-toggle="tab"> + <div class="icon-circle" [ngStyle]="getNavItemIconCircleStyle('datasets', datasetsStepChecked)"> + <span class="fas fa-book"></span> + </div> + Datasets + </a> + </li> + + <li class="nav-item" [ngClass]="{'active': currentStep === 'result', 'checked': resultStepChecked}"> + <a *ngIf="coneSearch && selectedDatasets.length > 0" class="nav-link" [ngStyle]="getNavItemAStyle('result', resultStepChecked)" routerLink="/instance/{{ instance.name }}/search-multiple/result" [queryParams]="queryParams" data-toggle="tab"> + <div class="icon-circle" [ngStyle]="getNavItemIconCircleStyle('result', resultStepChecked)"> + <span class="fas fa-table"></span> + </div> + Result + </a> + <a *ngIf="!coneSearch || selectedDatasets.length < 1" class="nav-link disabled" [ngStyle]="getNavItemAStyle('result', resultStepChecked)" data-toggle="tab"> + <div class="icon-circle" [ngStyle]="getNavItemIconCircleStyle('result', resultStepChecked)"> + <span class="fas fa-table"></span> + </div> + Result + </a> + </li> + </ul> +</div> diff --git a/client/src/app/instance/search-multiple/components/progress-bar-multiple.component.scss b/client/src/app/instance/search-multiple/components/progress-bar-multiple.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..0cf6548626fcaec4468e60ae9b438a7349060294 --- /dev/null +++ b/client/src/app/instance/search-multiple/components/progress-bar-multiple.component.scss @@ -0,0 +1,107 @@ +/** + * 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. + */ + +.progress-navigation { + position: relative; + height: 125px; + margin-top: 15px; +} + +.progress-with-circle { + position: relative; + top: 40px; + z-index: 50; + height: 4px; +} + +.progress-bar { + box-shadow: none; + -webkit-transition: width .3s ease; + -o-transition: width .3s ease; + transition: width .3s ease; +} + +.nav-pills { + background-color: #F3F2EE; + position: absolute; + width: 100%; + height: 4px; + top: 40px; + text-align: center; +} + +.nav-pills li a { + padding: 0; + max-width: 78px; + margin: 0 auto; + color: rgba(0, 0, 0, 0.2); + border-radius: 50%; + position: relative; + top: -32px; + z-index: 100; +} + +.icon-circle { + font-size: 20px; + border: 3px solid #E9ECEF; + text-align: center; + border-radius: 50%; + color: rgba(0, 0, 0, 0.2); + font-weight: 600; + width: 70px; + height: 70px; + background-color: #FFFFFF; + margin: 0 auto; + position: relative; + top: -2px; +} + +.nav-item { + width: 33%; +} + +.nav-item.active a { + background-color: transparent; +} + +.nav-item.active .icon-circle { + color: white !important; +} + +.nav-link.disabled { + cursor: not-allowed; +} + +.icon-circle svg { + position: absolute; + z-index: 1; + left: 22px; + right: 0; + top: 23px; +} + +.positionStep { + width: 15%; +} + +.datasetsStep { + width: 48%; +} + +.resultStep { + width: 100%; +} + +.btn-clear-form span { + display: none; +} + +.btn-clear-form:hover span { + display: inline; +} diff --git a/client/src/app/instance/search-multiple/components/progress-bar-multiple.component.ts b/client/src/app/instance/search-multiple/components/progress-bar-multiple.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..ece4925478828db29a2a9df8e097e3287ca2900e --- /dev/null +++ b/client/src/app/instance/search-multiple/components/progress-bar-multiple.component.ts @@ -0,0 +1,75 @@ +/** + * 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 { Instance } from 'src/app/metamodel/models'; +import { ConeSearch, SearchMultipleQueryParams } from 'src/app/instance/store/models'; + +@Component({ + selector: 'app-progress-bar-multiple', + templateUrl: 'progress-bar-multiple.component.html', + styleUrls: ['progress-bar-multiple.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +/** + * @class + * @classdesc Search multiple progress bar component. + */ +export class ProgressBarMultipleComponent { + @Input() instance: Instance; + @Input() currentStep: string; + @Input() positionStepChecked: boolean; + @Input() datasetsStepChecked: boolean; + @Input() resultStepChecked: boolean; + @Input() coneSearch: ConeSearch; + @Input() selectedDatasets: string[]; + @Input() queryParams: SearchMultipleQueryParams; + + /** + * Returns step class that match to the current step. + * + * @return string + */ + getStepClass(): string { + switch (this.currentStep) { + case 'position': + return 'positionStep'; + case 'datasets': + return 'datasetsStep'; + case 'result': + return 'resultStep'; + default: + return 'positionStep'; + } + } + + getNavItemAStyle(currentStep: string, checked: boolean) { + if (this.currentStep === currentStep || checked) { + return { + 'color': this.instance.config.design.design_color + } + } else { + return null; + } + } + + getNavItemIconCircleStyle(currentStep: string, checked: boolean) { + let style = {}; + if (this.currentStep === currentStep) { + style['border-color'] = this.instance.config.design.design_color; + style['background-color'] = this.instance.config.design.design_color; + } + if (checked) { + style['border-color'] = this.instance.config.design.design_color; + style['color'] = this.instance.config.design.design_color; + } + return style; + } +} diff --git a/client/src/app/instance/search-multiple/components/result/index.ts b/client/src/app/instance/search-multiple/components/result/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..73ec101031c08b3bdd8c58d04fed9afcb762ddac --- /dev/null +++ b/client/src/app/instance/search-multiple/components/result/index.ts @@ -0,0 +1,5 @@ +import { OverviewComponent } from './overview.component'; + +export const resultComponents = [ + OverviewComponent +]; diff --git a/client/src/app/instance/search-multiple/components/result/overview.component.html b/client/src/app/instance/search-multiple/components/result/overview.component.html new file mode 100644 index 0000000000000000000000000000000000000000..91b90040c64e0fa910f1e502ff2d7c33d34a5be1 --- /dev/null +++ b/client/src/app/instance/search-multiple/components/result/overview.component.html @@ -0,0 +1,75 @@ +<div class="jumbotron mb-5 py-4"> + <div *ngIf="getTotalObject() === 0"> + <div class=""> + <h2 class="font-weight-bold">No results.</h2> + </div> + <hr class="my-4"> + <div class="row justify-content-around"> + <div class="col-auto border-right"> + <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> + <div *ngFor="let datasetFamily of getSortedDatasetFamilyList()" class="col-auto"> + <span class="title">{{ datasetFamily.label }}</span> + <ul class="list-unstyled pl-3"> + <li *ngFor="let dataset of getSelectedDatasetsByFamily(datasetFamily.id)" > + {{ dataset.label }} <span class="badge badge-pill badge-light text-danger">0</span> + </li> + </ul> + </div> + </div> + <hr class="my-4"> + <div class="text-center"> + <a routerLink="/instance/{{ instanceSelected }}/search-multiple/position" class="btn btn-lg btn-outline-primary"> + <span class="fas fa-undo"></span> Try something else + </a> + </div> + </div> + + <div *ngIf="getTotalObject() > 0"> + <div class="lead"> + Found + <span class="font-weight-bold">{{ getTotalObject() }}</span> + <span *ngIf="getTotalObject() > 1; else object"> objects</span> + <ng-template #object> object</ng-template> + in + <span class="font-weight-bold">{{ getTotalDatasets() }}</span> + <span *ngIf="getTotalDatasets() > 1; else dataset"> datasets</span> + <ng-template #dataset> dataset</ng-template>. + </div> + <hr class="my-4"> + <div class="row justify-content-around"> + <div class="col-auto border-right"> + <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> + <div *ngFor="let datasetFamily of getSortedDatasetFamilyList()" class="col-auto"> + <span class="title">{{ datasetFamily.label }}</span> + <ul class="list-unstyled pl-3"> + <li *ngFor="let dataset of getSelectedDatasetsByFamily(datasetFamily.id)" > + {{ dataset.label }} + <span class="badge badge-pill badge-light" [ngClass]="{'text-primary': getCountByDataset(dataset.name) !== 0}"> + {{ getCountByDataset(dataset.name) }} + </span> + <span *ngIf="getCountByDataset(dataset.name) < 2"> object found</span> + <span *ngIf="getCountByDataset(dataset.name) > 1"> objects found</span> + <hr *ngIf="getCountByDataset(dataset.name) > 0" class="my-4"> + <div *ngIf="getCountByDataset(dataset.name) > 0" class="text-center"> + <a routerLink="/instance/{{ instanceSelected }}/search/result/{{ dataset.name }}" [queryParams]="getCsQueryParams()" class="btn btn-outline-primary"> + <span class="fas fa-forward"></span> Go to result + </a> + </div> + </li> + </ul> + </div> + </div> + </div> +</div> diff --git a/client/src/app/instance/search-multiple/components/result/overview.component.ts b/client/src/app/instance/search-multiple/components/result/overview.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e85af4d2feed7f641e352c6f5a998a7a596899c8 --- /dev/null +++ b/client/src/app/instance/search-multiple/components/result/overview.component.ts @@ -0,0 +1,94 @@ +/** + * 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 { DatasetFamily, Dataset } from 'src/app/metamodel/models'; +import { ConeSearch, SearchMultipleDatasetLength, SearchMultipleQueryParams } from 'src/app/instance/store/models'; + +@Component({ + selector: 'app-overview', + templateUrl: 'overview.component.html' +}) +export class OverviewComponent { + @Input() instanceSelected: string; + @Input() datasetFamilyList: DatasetFamily[]; + @Input() datasetList: Dataset[]; + @Input() coneSearch: ConeSearch; + @Input() selectedDatasets: string[]; + @Input() dataLength: SearchMultipleDatasetLength[]; + @Input() queryParams: SearchMultipleQueryParams; + + /** + * Returns total amount of results for all datasets. + * + * @return number + */ + getTotalObject(): number { + return this.dataLength + .filter(datasetLength => datasetLength.length > 0) + .reduce((sum, datasetLength) => sum + datasetLength.length, 0); + } + + /** + * Returns total number of datasets with results. + * + * @return number + */ + getTotalDatasets(): number { + return this.dataLength.filter(datasetLength => datasetLength.length > 0).length; + } + + /** + * Returns dataset families sorted by display, that contains selected datasets. + * + * @return Family[] + */ + getSortedDatasetFamilyList(): DatasetFamily[] { + let datasetFamiliesWithSelectedDataset: DatasetFamily[] = []; + this.selectedDatasets.forEach(dname => { + const dataset: Dataset = this.datasetList.find(d => d.name === dname); + const datasetFamily: DatasetFamily = this.datasetFamilyList.find(f => f.id === dataset.id_dataset_family); + if (!datasetFamiliesWithSelectedDataset.includes(datasetFamily)) { + datasetFamiliesWithSelectedDataset.push(datasetFamily); + } + }); + return datasetFamiliesWithSelectedDataset; + } + + /** + * Returns selected dataset list for the given dataset family ID. + * + * @param {number} familyId - The family ID. + * + * @return Dataset[] + */ + getSelectedDatasetsByFamily(familyId: number): Dataset[] { + return this.datasetList + .filter(d => d.id_dataset_family === familyId) + .filter(d => this.selectedDatasets.includes(d.name)); + } + + /** + * Returns the result number for the given dataset name. + * + * @param {string} dname - The dataset name. + * + * @return number + */ + getCountByDataset(dname: string): number { + return this.dataLength.find(datasetLength => datasetLength.datasetName === dname).length; + } + + getCsQueryParams() { + return { + cs: `${this.coneSearch.ra}:${this.coneSearch.dec}:${this.coneSearch.radius}` + } + } +} diff --git a/client/src/app/instance/search-multiple/components/summary-multiple.component.html b/client/src/app/instance/search-multiple/components/summary-multiple.component.html new file mode 100644 index 0000000000000000000000000000000000000000..3d60496597bb4fde521dab330da270201431178a --- /dev/null +++ b/client/src/app/instance/search-multiple/components/summary-multiple.component.html @@ -0,0 +1,59 @@ +<div class="border rounded"> + <p class="lead text-center border-bottom bg-light py-3">Search summary</p> + + <!-- Position --> + <p class="text-center font-italic"> + Position + </p> + <span *ngIf="coneSearch" class="pl-5"> + Cone search: + <ul class="ml-3 pl-5 list-unstyled"> + <li>RA = {{ coneSearch.ra }}°</li> + <li>DEC = {{ coneSearch.dec }}°</li> + <li>radius = {{ coneSearch.radius }} arcsecond</li> + </ul> + </span> + <p *ngIf="!coneSearch" class="pl-5 text-danger font-weight-bold"> + Not valid position! + </p> + <hr> + + <!-- Dataset List --> + <p class="text-center font-italic"> + Datasets + </p> + <div> + <p *ngIf="selectedDatasets.length < 1" class="pl-5 text-danger font-weight-bold"> + At least 1 dataset required! + </p> + <div *ngIf="selectedDatasets.length > 0"> + <!-- Accordion Dataset families --> + <accordion [isAnimated]="true"> + <accordion-group *ngFor="let datasetFamily of getDatasetFamilyList()" #ag panelClass="abstract-accordion" [isOpen]="accordionFamilyIsOpen" class="pl-5"> + <button class="btn btn-link btn-block clearfix pb-1 text-primary" accordion-heading> + <div class="pull-left float-left"> + {{ datasetFamily.label }} + + <span *ngIf="ag.isOpen"> + <span class="fas fa-chevron-up"></span> + </span> + <span *ngIf="!ag.isOpen"> + <span class="fas fa-chevron-down"></span> + </span> + </div> + </button> + + <!-- Selected Datasets --> + <ul *ngIf="getSelectedDatasetsByFamily(datasetFamily.id).length > 0; else noDataset" class="mb-0 pl-4 list-unstyled"> + <li *ngFor="let dataset of getSelectedDatasetsByFamily(datasetFamily.id)" class="pb-1"> + {{ dataset.label }} + </li> + </ul> + <ng-template #noDataset> + <p class="mb-1 pl-4">No selected dataset</p> + </ng-template> + </accordion-group> + </accordion> + </div> + </div> +</div> diff --git a/client/src/app/instance/search-multiple/components/summary-multiple.component.scss b/client/src/app/instance/search-multiple/components/summary-multiple.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..c837d14e1ce994aeb0857803139aaad3fd7f8d6d --- /dev/null +++ b/client/src/app/instance/search-multiple/components/summary-multiple.component.scss @@ -0,0 +1,12 @@ +/** + * 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. + */ + +li>span { + cursor: pointer; +} diff --git a/client/src/app/instance/search-multiple/components/summary-multiple.component.ts b/client/src/app/instance/search-multiple/components/summary-multiple.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..790fcd5c75c8862f0dab3bef3ff867edc4c7a361 --- /dev/null +++ b/client/src/app/instance/search-multiple/components/summary-multiple.component.ts @@ -0,0 +1,64 @@ +/** + * 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, Input } from '@angular/core'; + +import { SearchMultipleQueryParams, ConeSearch } from 'src/app/instance/store/models'; +import { Dataset, DatasetFamily } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-summary-multiple', + templateUrl: 'summary-multiple.component.html', + styleUrls: ['summary-multiple.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +/** + * @class + * @classdesc Search multiple summary component. + */ +export class SummaryMultipleComponent { + @Input() currentStep: string; + @Input() coneSearch: ConeSearch; + @Input() selectedDatasets: string[]; + @Input() queryParams: SearchMultipleQueryParams; + @Input() datasetFamilyList: DatasetFamily[]; + @Input() datasetList: Dataset[]; + + accordionFamilyIsOpen = true; + + /** + * Returns dataset families sorted by display, that contains datasets with cone search enabled. + * + * @return Family[] + */ + getDatasetFamilyList(): DatasetFamily[] { + const familiesId: number[] = []; + this.datasetList.forEach(d => { + if (!familiesId.includes(d.id_dataset_family)) { + familiesId.push(d.id_dataset_family); + } + }); + return this.datasetFamilyList + .filter(f => familiesId.includes(f.id)) + //.sort(sortByDisplay); + } + + /** + * Returns dataset list for the given dataset family ID. + * + * @param {number} familyId - The family ID. + * + * @return Dataset[] + */ + getSelectedDatasetsByFamily(familyId: number): Dataset[] { + return this.datasetList + .filter(d => d.id_dataset_family === familyId) + .filter(d => this.selectedDatasets.includes(d.name)); + } +} diff --git a/client/src/app/instance/search-multiple/containers/abstract-search-multiple.component.ts b/client/src/app/instance/search-multiple/containers/abstract-search-multiple.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d8bbadd476b404c8b20a43d728284d0423c3a67f --- /dev/null +++ b/client/src/app/instance/search-multiple/containers/abstract-search-multiple.component.ts @@ -0,0 +1,58 @@ +import { Directive, OnDestroy, OnInit } from '@angular/core'; + +import { Store } from '@ngrx/store'; +import { Observable, Subscription } from 'rxjs'; + +import { Dataset, DatasetFamily } from 'src/app/metamodel/models'; +import { ConeSearch, SearchMultipleQueryParams } from '../../store/models'; +import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; +import * as datasetFamilySelector from 'src/app/metamodel/selectors/dataset-family.selector'; +import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector'; +import * as searchMultipleActions from '../../store/actions/search-multiple.actions'; +import * as searchMultipleSelector from 'src/app/instance/store/selectors/search-multiple.selector'; +import * as coneSearchSelector from 'src/app/instance/store/selectors/cone-search.selector'; + +@Directive() +export abstract class AbstractSearchMultipleComponent implements OnInit, OnDestroy { + public pristine: Observable<boolean>; + public instanceSelected: Observable<string>; + public datasetFamilyListIsLoading: Observable<boolean>; + public datasetFamilyListIsLoaded: Observable<boolean>; + public datasetFamilyList: Observable<DatasetFamily[]>; + public datasetListIsLoading: Observable<boolean>; + public datasetListIsLoaded: Observable<boolean>; + public datasetList: Observable<Dataset[]>; + public currentStep: Observable<string>; + public selectedDatasets: Observable<string[]>; + public coneSearch: Observable<ConeSearch>; + public queryParams: Observable<SearchMultipleQueryParams>; + + private datasetListSubscription: Subscription; + + constructor(protected store: Store<{ }>) { + this.pristine = this.store.select(searchMultipleSelector.selectPristine); + this.instanceSelected = this.store.select(instanceSelector.selectInstanceNameByRoute); + this.datasetFamilyListIsLoading = store.select(datasetFamilySelector.selectDatasetFamilyListIsLoading); + this.datasetFamilyListIsLoaded = store.select(datasetFamilySelector.selectDatasetFamilyListIsLoaded); + this.datasetFamilyList = store.select(datasetFamilySelector.selectAllDatasetFamilies); + this.datasetListIsLoading = this.store.select(datasetSelector.selectDatasetListIsLoading); + this.datasetListIsLoaded = this.store.select(datasetSelector.selectDatasetListIsLoaded); + this.datasetList = this.store.select(datasetSelector.selectAllConeSearchDatasets); + this.currentStep = this.store.select(searchMultipleSelector.selectCurrentStep) + this.selectedDatasets = this.store.select(searchMultipleSelector.selectSelectedDatasets); + this.coneSearch = this.store.select(coneSearchSelector.selectConeSearch); + this.queryParams = this.store.select(searchMultipleSelector.selectQueryParams); + } + + ngOnInit() { + this.datasetListSubscription = this.datasetListIsLoaded.subscribe(datasetListIsLoaded => { + if (datasetListIsLoaded) { + Promise.resolve(null).then(() => this.store.dispatch(searchMultipleActions.initSearch())); + } + }) + } + + ngOnDestroy() { + this.datasetListSubscription.unsubscribe(); + } +} diff --git a/client/src/app/instance/search-multiple/containers/datasets.component.html b/client/src/app/instance/search-multiple/containers/datasets.component.html new file mode 100644 index 0000000000000000000000000000000000000000..6c030e4a4f8ca0b8ec8d5ce94b654778d7baf1f0 --- /dev/null +++ b/client/src/app/instance/search-multiple/containers/datasets.component.html @@ -0,0 +1,44 @@ +<app-spinner *ngIf="(datasetFamilyListIsLoading | async) || (datasetListIsLoading | async) || (surveyListIsLoading | async)"> +</app-spinner> + +<div *ngIf="(datasetFamilyListIsLoaded | async) && (datasetListIsLoaded | async) && (surveyListIsLoaded | async)" class="row mt-4"> + <div class="col-12 col-md-8 col-lg-9"> + <div class="border rounded my-2"> + <p class="border-bottom bg-light text-primary mb-0 py-4 pl-4">Datasets</p> + <div class="px-3"> + <app-dataset-list + [surveyList]="surveyList | async" + [datasetFamilyList]="datasetFamilyList | async" + [datasetList]="datasetList | async" + [selectedDatasets]="selectedDatasets | async" + (updateSelectedDatasets)="updateSelectedDatasets($event)"> + </app-dataset-list> + </div> + </div> + </div> + <div class="col-12 col-md-4 col-lg-3 pt-2"> + <app-summary-multiple + [currentStep]="currentStep | async" + [coneSearch]="coneSearch | async" + [selectedDatasets]="selectedDatasets | async" + [queryParams]="queryParams | async" + [datasetFamilyList]="datasetFamilyList | async" + [datasetList]="datasetList | async"> + </app-summary-multiple> + </div> +</div> + +<div class="row mt-5 justify-content-between"> + <div class="col"> + <a routerLink="/instance/{{ instanceSelected | async }}/search-multiple/position" [queryParams]="queryParams | async" + class="btn btn-outline-secondary"> + <span class="fas fa-arrow-left"></span> Position + </a> + </div> + <div *ngIf="(selectedDatasets | async).length > 0" class="col col-auto"> + <a routerLink="/instance/{{ instanceSelected | async }}/search-multiple/result" [queryParams]="queryParams | async" + class="btn btn-outline-primary"> + Result <span class="fas fa-arrow-right"></span> + </a> + </div> +</div> diff --git a/client/src/app/instance/search-multiple/containers/datasets.component.ts b/client/src/app/instance/search-multiple/containers/datasets.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b990465122fc275552cdd57367d0299df39bccc --- /dev/null +++ b/client/src/app/instance/search-multiple/containers/datasets.component.ts @@ -0,0 +1,45 @@ +/** + * 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 } from '@angular/core'; + +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; + +import { Survey } from 'src/app/metamodel/models'; +import { AbstractSearchMultipleComponent } from './abstract-search-multiple.component'; +import * as searchMultipleActions from '../../store/actions/search-multiple.actions'; +import * as surveySelector from 'src/app/metamodel/selectors/survey.selector'; + +@Component({ + selector: 'app-datasets', + templateUrl: 'datasets.component.html' +}) +export class DatasetsComponent extends AbstractSearchMultipleComponent { + public surveyListIsLoading: Observable<boolean>; + public surveyListIsLoaded: Observable<boolean>; + public surveyList: Observable<Survey[]>; + + constructor(protected store: Store<{ }>) { + super(store); + this.surveyListIsLoading = store.select(surveySelector.selectSurveyListIsLoading); + this.surveyListIsLoaded = store.select(surveySelector.selectSurveyListIsLoaded); + this.surveyList = store.select(surveySelector.selectAllSurveys); + } + + ngOnInit() { + Promise.resolve(null).then(() => this.store.dispatch(searchMultipleActions.changeStep({ step: 'datasets' }))); + Promise.resolve(null).then(() => this.store.dispatch(searchMultipleActions.checkDatasets())); + super.ngOnInit(); + } + + updateSelectedDatasets(selectedDatasets: string[]) { + this.store.dispatch(searchMultipleActions.updateSelectedDatasets({ selectedDatasets })); + } +} diff --git a/client/src/app/instance/search-multiple/containers/position.component.html b/client/src/app/instance/search-multiple/containers/position.component.html new file mode 100644 index 0000000000000000000000000000000000000000..c12f7eee58be2fcc472afa3b853db897c874d433 --- /dev/null +++ b/client/src/app/instance/search-multiple/containers/position.component.html @@ -0,0 +1,47 @@ +<app-spinner *ngIf="(pristine | async) || (datasetFamilyListIsLoading | async) || (datasetListIsLoading | async)"> +</app-spinner> + +<div *ngIf="!(pristine | async) && (datasetFamilyListIsLoaded | async) && (datasetListIsLoaded | async)" class="row mt-4"> + <div class="col-12 col-md-8 col-lg-9"> + <div class="border rounded my-2"> + <p class="border-bottom bg-light text-primary mb-0 py-4 pl-4">Cone Search</p> + <div class="row p-4"> + <div class="col"> + <app-cone-search + [coneSearch]="coneSearch | async" + [resolver]="resolver | async" + [resolverIsLoading]="resolverIsLoading | async" + [resolverIsLoaded]="resolverIsLoaded | async" + (retrieveCoordinates)="retrieveCoordinates($event)" #cs> + </app-cone-search> + </div> + <div class="col-2 text-center align-self-end"> + <button class="btn btn-outline-success" *ngIf="!(coneSearch | async)" [hidden]="cs.form.invalid" (click)="addConeSearch(cs.getConeSearch())"> + <span class="fas fa-plus fa-fw"></span> + </button> + <button class="btn btn-outline-danger" *ngIf="coneSearch | async" (click)="deleteConeSearch()"> + <span class="fa fa-times fa-fw"></span> + </button> + </div> + </div> + </div> + </div> + <div class="col-12 col-md-4 col-lg-3 pt-2"> + <app-summary-multiple + [currentStep]="currentStep | async" + [coneSearch]="coneSearch | async" + [selectedDatasets]="selectedDatasets | async" + [queryParams]="queryParams | async" + [datasetFamilyList]="datasetFamilyList | async" + [datasetList]="datasetList | async"> + </app-summary-multiple> + </div> +</div> + +<div *ngIf="coneSearch | async" class="row mt-5 justify-content-end"> + <div class="col col-auto"> + <a routerLink="/instance/{{ instanceSelected | async }}/search-multiple/datasets" [queryParams]="queryParams | async" + class="btn btn-outline-primary">Datasets <span class="fas fa-arrow-right"></span> + </a> + </div> +</div> diff --git a/client/src/app/instance/search-multiple/containers/position.component.ts b/client/src/app/instance/search-multiple/containers/position.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..dea597950d3f0c527909d7059537152eda0c00f8 --- /dev/null +++ b/client/src/app/instance/search-multiple/containers/position.component.ts @@ -0,0 +1,53 @@ +/** + * 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 } from '@angular/core'; + +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; + +import { AbstractSearchMultipleComponent } from './abstract-search-multiple.component'; +import { Resolver, ConeSearch } from '../../store/models'; +import * as searchMultipleActions from '../../store/actions/search-multiple.actions'; +import * as coneSearchActions from '../../store/actions/cone-search.actions'; +import * as coneSearchSelector from '../../store/selectors/cone-search.selector'; + +@Component({ + selector: 'app-position', + templateUrl: 'position.component.html' +}) +export class PositionComponent extends AbstractSearchMultipleComponent { + public resolver: Observable<Resolver>; + public resolverIsLoading: Observable<boolean>; + public resolverIsLoaded: Observable<boolean>; + + constructor(protected store: Store<{ }>) { + super(store); + this.resolver = this.store.select(coneSearchSelector.selectResolver); + this.resolverIsLoading = this.store.select(coneSearchSelector.selectResolverIsLoading); + this.resolverIsLoaded = this.store.select(coneSearchSelector.selectResolverIsLoaded); + } + + ngOnInit() { + Promise.resolve(null).then(() => this.store.dispatch(searchMultipleActions.changeStep({ step: 'position' }))); + super.ngOnInit(); + } + + addConeSearch(coneSearch: ConeSearch): void { + this.store.dispatch(coneSearchActions.addConeSearch({ coneSearch })); + } + + deleteConeSearch(): void { + this.store.dispatch(coneSearchActions.deleteConeSearch()); + } + + retrieveCoordinates(name: string): void { + this.store.dispatch(coneSearchActions.retrieveCoordinates({ name })); + } +} diff --git a/client/src/app/instance/search-multiple/containers/result-multiple.component.html b/client/src/app/instance/search-multiple/containers/result-multiple.component.html new file mode 100644 index 0000000000000000000000000000000000000000..258f1567d2d3f55ae3fba8ef57b014824078f4a8 --- /dev/null +++ b/client/src/app/instance/search-multiple/containers/result-multiple.component.html @@ -0,0 +1,41 @@ +<app-spinner *ngIf="(datasetFamilyListIsLoading | async) || (datasetListIsLoading | async) || (dataLengthIsLoading | async)"> +</app-spinner> + +<div *ngIf="(datasetFamilyListIsLoaded | async) && (datasetListIsLoaded | async) || (dataLengthIsLoaded | async)" class="row mt-4"> + <div class="col-12"> + <app-overview + [instanceSelected]="instanceSelected | async" + [datasetFamilyList]="datasetFamilyList | async" + [datasetList]="datasetList | async" + [coneSearch]="coneSearch | async" + [selectedDatasets]="selectedDatasets | async" + [dataLength]="dataLength | async" + [queryParams]="queryParams | async"> + </app-overview> + <!-- + <app-datasets-result + [datasetsCountIsLoaded]="datasetsCountIsLoaded | async" + [datasetFamilyList]="datasetFamilyList | async" + [datasetList]="datasetList | async" + [coneSearch]="coneSearch | async" + [selectedDatasets]="selectedDatasets | async" + [datasetsCount]="datasetsCount | async" + [datasetsWithAttributeList]="datasetsWithAttributeList | async" + [allAttributeList]="allAttributeList | async" + [datasetsWithData]="datasetsWithData | async" + [allData]="allData | async" + [selectedData]="selectedData | async" + (retrieveMeta)="retrieveMeta($event)" + (retrieveData)="retrieveData($event)" + (updateSelectedData)="updateSelectedData($event)"> + </app-datasets-result> --> + </div> +</div> +<div class="row mt-5 justify-content-between"> + <div class="col"> + <a routerLink="/instance/{{ instanceSelected | async }}/search-multiple/datasets" [queryParams]="queryParams | async" + class="btn btn-outline-secondary"> + <span class="fas fa-arrow-left"></span> Datasets + </a> + </div> +</div> diff --git a/client/src/app/instance/search-multiple/containers/result-multiple.component.ts b/client/src/app/instance/search-multiple/containers/result-multiple.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3e4d66b40fbfcad6aabcededb30c6f628554f20d --- /dev/null +++ b/client/src/app/instance/search-multiple/containers/result-multiple.component.ts @@ -0,0 +1,60 @@ +/** + * 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 } from '@angular/core'; + +import { Observable, Subscription } from 'rxjs'; +import { Store } from '@ngrx/store'; + +import { SearchMultipleDatasetLength, SearchMultipleDatasetData } from '../../store/models'; +import { AbstractSearchMultipleComponent } from './abstract-search-multiple.component'; +import * as searchMultipleActions from '../../store/actions/search-multiple.actions'; +import * as searchMultipleSelector from 'src/app/instance/store/selectors/search-multiple.selector'; + +@Component({ + selector: 'app-result-multiple', + templateUrl: 'result-multiple.component.html' +}) +export class ResultMultipleComponent extends AbstractSearchMultipleComponent { + public dataLength: Observable<SearchMultipleDatasetLength[]>; + public dataLengthIsLoading: Observable<boolean>; + public dataLengthIsLoaded: Observable<boolean>; + public data: Observable<SearchMultipleDatasetData[]>; + public dataIsLoading: Observable<boolean>; + public dataIsLoaded: Observable<boolean>; + + private pristineSubscription: Subscription; + + constructor(protected store: Store<{ }>) { + super(store); + this.dataLength = this.store.select(searchMultipleSelector.selectDataLength); + this.dataLengthIsLoading = this.store.select(searchMultipleSelector.selectDataLengthIsLoading); + this.dataLengthIsLoaded = this.store.select(searchMultipleSelector.selectDataLengthIsLoaded); + this.data = this.store.select(searchMultipleSelector.selectData); + this.dataIsLoading = this.store.select(searchMultipleSelector.selectDataIsLoading); + this.dataIsLoaded = this.store.select(searchMultipleSelector.selectDataIsLoaded); + } + + ngOnInit() { + Promise.resolve(null).then(() => this.store.dispatch(searchMultipleActions.changeStep({ step: 'result' }))); + Promise.resolve(null).then(() => this.store.dispatch(searchMultipleActions.checkDatasets())); + Promise.resolve(null).then(() => this.store.dispatch(searchMultipleActions.checkResult())); + super.ngOnInit(); + this.pristineSubscription = this.pristine.subscribe(pristine => { + if (!pristine) { + Promise.resolve(null).then(() => this.store.dispatch(searchMultipleActions.retrieveDataLength())); + } + }); + } + + ngOnDestroy() { + this.pristineSubscription.unsubscribe(); + super.ngOnDestroy(); + } +} diff --git a/client/src/app/instance/search-multiple/search-multiple-routing.module.ts b/client/src/app/instance/search-multiple/search-multiple-routing.module.ts index 4438a68664b0e8e931aabab371af2a8841fc845f..a3590eb28c7d96bbd48addb36fa15a9d00c3ff0b 100644 --- a/client/src/app/instance/search-multiple/search-multiple-routing.module.ts +++ b/client/src/app/instance/search-multiple/search-multiple-routing.module.ts @@ -10,10 +10,20 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -//import { SearchMultipleComponent } from './search-multiple.component'; +import { SearchMultipleComponent } from './search-multiple.component'; +import { PositionComponent } from './containers/position.component'; +import { DatasetsComponent } from './containers/datasets.component'; +import { ResultMultipleComponent } from './containers/result-multiple.component'; const routes: Routes = [ - //{ path: '', component: SearchMultipleComponent } + { + path: '', component: SearchMultipleComponent, children: [ + { path: '', redirectTo: 'position', pathMatch: 'full' }, + { path: 'position', component: PositionComponent }, + { path: 'datasets', component: DatasetsComponent }, + { path: 'result', component: ResultMultipleComponent } + ] + } ]; @NgModule({ @@ -23,5 +33,8 @@ const routes: Routes = [ export class SearchMultipleRoutingModule { } export const routedComponents = [ - //SearchMultipleComponent + SearchMultipleComponent, + PositionComponent, + DatasetsComponent, + ResultMultipleComponent ]; diff --git a/client/src/app/instance/search-multiple/search-multiple.component.html b/client/src/app/instance/search-multiple/search-multiple.component.html new file mode 100644 index 0000000000000000000000000000000000000000..7f7342be55b65395e810bc751799628776dea30e --- /dev/null +++ b/client/src/app/instance/search-multiple/search-multiple.component.html @@ -0,0 +1,13 @@ +<div class="mx-1 mx-sm-5 px-1 px-sm-5"> + <app-progress-bar-multiple + [instance]="instance | async" + [currentStep]="currentStep | async" + [positionStepChecked]="positionStepChecked | async" + [datasetsStepChecked]="datasetsStepChecked | async" + [resultStepChecked]="resultStepChecked | async" + [coneSearch]="coneSearch | async" + [selectedDatasets]="selectedDatasets | async" + [queryParams]="queryParams | async"> + </app-progress-bar-multiple> + <router-outlet></router-outlet> +</div> diff --git a/client/src/app/instance/search-multiple/search-multiple.component.ts b/client/src/app/instance/search-multiple/search-multiple.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..b0d2b7b88a44d5099b04b2455761c63191ecc8da --- /dev/null +++ b/client/src/app/instance/search-multiple/search-multiple.component.ts @@ -0,0 +1,49 @@ +/** + * 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, } from '@angular/core'; + +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; + +import { ConeSearch, SearchMultipleQueryParams } from '../store/models'; +import { Instance } from 'src/app/metamodel/models'; +import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; +import * as searchMultipleSelector from '../store/selectors/search-multiple.selector'; +import * as coneSearchSelector from '../store/selectors/cone-search.selector'; + +@Component({ + selector: 'app-search-multiple', + templateUrl: 'search-multiple.component.html' +}) +/** + * @class + * @classdesc Search multiple container. + */ +export class SearchMultipleComponent { + public instance: Observable<Instance>; + public currentStep: Observable<string>; + public positionStepChecked: Observable<boolean>; + public datasetsStepChecked: Observable<boolean>; + public resultStepChecked: Observable<boolean>; + public coneSearch: Observable<ConeSearch>; + public selectedDatasets: Observable<string[]>; + public queryParams: Observable<SearchMultipleQueryParams>; + + constructor(private store: Store<{ }>) { + this.instance = this.store.select(instanceSelector.selectInstanceByRouteName); + this.currentStep = this.store.select(searchMultipleSelector.selectCurrentStep); + this.positionStepChecked = this.store.select(searchMultipleSelector.selectPositionStepChecked); + this.datasetsStepChecked = this.store.select(searchMultipleSelector.selectDatasetsStepChecked); + this.resultStepChecked = this.store.select(searchMultipleSelector.selectResultStepChecked); + this.coneSearch = this.store.select(coneSearchSelector.selectConeSearch); + this.selectedDatasets = this.store.select(searchMultipleSelector.selectSelectedDatasets); + this.queryParams = this.store.select(searchMultipleSelector.selectQueryParams); + } +} diff --git a/client/src/app/instance/search-multiple/search-multiple.module.ts b/client/src/app/instance/search-multiple/search-multiple.module.ts index 6368aaf5ac0eed7e0f4be27e5839c470efd21e25..f309487d7a04e1e064c428c6a1303379f093d57d 100644 --- a/client/src/app/instance/search-multiple/search-multiple.module.ts +++ b/client/src/app/instance/search-multiple/search-multiple.module.ts @@ -10,13 +10,19 @@ import { NgModule } from '@angular/core'; import { SharedModule } from 'src/app/shared/shared.module'; +import { SharedSearchModule } from '../shared-search/shared-search.module'; import { SearchMultipleRoutingModule, routedComponents } from './search-multiple-routing.module'; +import { dummiesComponents } from './components'; @NgModule({ imports: [ SharedModule, + SharedSearchModule, SearchMultipleRoutingModule ], - declarations: [routedComponents] + declarations: [ + routedComponents, + dummiesComponents + ] }) export class SearchMultipleModule { } diff --git a/client/src/app/instance/search/components/criteria/cone-search-tab.component.html b/client/src/app/instance/search/components/criteria/cone-search-tab.component.html index ea951c32bd3106f1c745ad5e18876c73d592bad5..f3ec7c1649ba70a4fb2d5a3c4ae111451c49fd15 100644 --- a/client/src/app/instance/search/components/criteria/cone-search-tab.component.html +++ b/client/src/app/instance/search/components/criteria/cone-search-tab.component.html @@ -19,8 +19,6 @@ [resolver]="resolver" [resolverIsLoading]="resolverIsLoading" [resolverIsLoaded]="resolverIsLoaded" - (addConeSearch)="addConeSearch.emit($event)" - (deleteConeSearch)="deleteConeSearch.emit()" (retrieveCoordinates)="retrieveCoordinates.emit($event)" #cs> </app-cone-search> </div> diff --git a/client/src/app/instance/search/components/criteria/criteria-by-family.component.html b/client/src/app/instance/search/components/criteria/criteria-by-family.component.html index dd9b10ecfa1ba5ad05f4bef58d948d7eb59a8165..08f385e6d3361791bd67c7e2ae7f2ec88dfddbea 100644 --- a/client/src/app/instance/search/components/criteria/criteria-by-family.component.html +++ b/client/src/app/instance/search/components/criteria/criteria-by-family.component.html @@ -1,4 +1,4 @@ -<div *ngFor="let attribute of getAttributeListSortedByDisplay()"> +<div *ngFor="let attribute of attributeList"> <div [ngSwitch]="attribute.search_type"> <div *ngSwitchCase="'field'"> <app-field class="criteria" diff --git a/client/src/app/instance/search/components/criteria/criteria-by-family.component.ts b/client/src/app/instance/search/components/criteria/criteria-by-family.component.ts index ed525e72ea70a3f5e38775b170ccf2811cb12dcd..b249169a910b240ef5c848f78f9bc37cbfa6ad79 100644 --- a/client/src/app/instance/search/components/criteria/criteria-by-family.component.ts +++ b/client/src/app/instance/search/components/criteria/criteria-by-family.component.ts @@ -29,16 +29,6 @@ export class CriteriaByFamilyComponent { advancedForm = false; - /** - * Returns attribute list sorted by criteria display. - * - * @return Attribute[] - */ - getAttributeListSortedByDisplay(): Attribute[] { - return this.attributeList - .sort((a, b) => a.criteria_display - b.criteria_display); - } - /** * Returns options for the given attribute ID. * diff --git a/client/src/app/instance/search/components/dataset/dataset-card.component.html b/client/src/app/instance/search/components/dataset/dataset-card.component.html index 190c9bc953e14a62ffc4528a79053ed03dece876..07daf796a9f9c039bfa6c6f3594d250604c57646 100644 --- a/client/src/app/instance/search/components/dataset/dataset-card.component.html +++ b/client/src/app/instance/search/components/dataset/dataset-card.component.html @@ -10,7 +10,7 @@ <div class="row"> <p class="my-3">{{ dataset.description }}</p> </div> - <div class="row"> + <div *ngIf="dataset.config.survey.survey_enabled" class="row"> <button class="btn btn-link p-0" popover="{{ survey.description }}" popoverTitle="{{ survey.label }}" @@ -18,7 +18,7 @@ [outsideClick]="true" triggers="mouseenter:mouseleave"> <small> - More about {{ survey.label }} survey <span class="fas fa-question-circle"></span> + {{ dataset.config.survey.survey_label }} <span class="fas fa-question-circle"></span> </small> </button> </div> diff --git a/client/src/app/instance/search/components/output/output-by-category.component.html b/client/src/app/instance/search/components/output/output-by-category.component.html index 7888b9a0acb076a724a631a25a3ef7095f667414..9ffecf619e406e41644270f2d15cbd97c0639770 100644 --- a/client/src/app/instance/search/components/output/output-by-category.component.html +++ b/client/src/app/instance/search/components/output/output-by-category.component.html @@ -14,7 +14,7 @@ </div> </div> <div class="selectbox p-0"> - <div *ngFor="let attribute of getAttributeListSortedByDisplay()"> + <div *ngFor="let attribute of attributeList"> <div *ngIf="isSelected(attribute.id)"> <button class="btn btn-block text-left py-1 m-0 rounded-0" (click)="toggleSelection(attribute.id)"> <span class="fas fa-fw fa-check-square" [ngStyle]="{ 'color': designColor }"></span> {{ attribute.form_label }} diff --git a/client/src/app/instance/search/components/output/output-by-category.component.ts b/client/src/app/instance/search/components/output/output-by-category.component.ts index 3cfd66d0ad7da4de3456540fb4f15bf210fad09a..830f1495803575e40d45722c797581fa68ec7e56 100644 --- a/client/src/app/instance/search/components/output/output-by-category.component.ts +++ b/client/src/app/instance/search/components/output/output-by-category.component.ts @@ -30,16 +30,6 @@ export class OutputByCategoryComponent { @Input() isAllUnselected: boolean; @Output() change: EventEmitter<number[]> = new EventEmitter(); - /** - * Returns output list sorted by output display. - * - * @return Attribute[] - */ - getAttributeListSortedByDisplay(): Attribute[] { - return this.attributeList - .sort((a, b) => a.output_display - b.output_display); - } - /** * Checks if the given output ID is selected. * diff --git a/client/src/app/instance/search/components/output/output-by-family.component.ts b/client/src/app/instance/search/components/output/output-by-family.component.ts index 23ff1ee0def5dc3eab3841d656115303b1e3f9c5..331058f4052dcfda301b16856e7c8527164b96ce 100644 --- a/client/src/app/instance/search/components/output/output-by-family.component.ts +++ b/client/src/app/instance/search/components/output/output-by-family.component.ts @@ -89,7 +89,6 @@ export class OutputByFamilyComponent { this.change.emit( this.attributeList .filter(a => clonedOutputList.indexOf(a.id) > -1) - .sort((a, b) => a.output_display - b.output_display) .map(a => a.id) ); } 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 4f9d94496d025ae285d6bd9a7c30ad0e1db42fe8..5fc5683f29b4555db3b7c386b4ac69443785314b 100644 --- a/client/src/app/instance/search/components/result/download.component.html +++ b/client/src/app/instance/search/components/result/download.component.html @@ -18,15 +18,15 @@ <p>Download results just here:</p> </div> <div class="col"> - <a *ngIf="getConfigDownloadResultFormat('download_csv')" [href]="getUrl('csv')" class="btn btn-outline-primary" title="Download results in CSV format"> + <a *ngIf="getConfigDownloadResultFormat('download_csv')" [href]="getUrl('csv')" (click)="click($event, getUrl('csv'), 'csv')" class="btn btn-outline-primary" title="Download results in CSV format"> <i class="fas fa-file-csv"></i> CSV </a> - <a *ngIf="getConfigDownloadResultFormat('download_ascii')" [href]="getUrl('ascii')" target="_blank" class="btn btn-outline-primary" title="Download results in ASCII format"> + <a *ngIf="getConfigDownloadResultFormat('download_ascii')" [href]="getUrl('ascii')" (click)="click($event, getUrl('ascii'), 'txt')" class="btn btn-outline-primary" title="Download results in ASCII format"> <i class="fas fa-file"></i> ASCII </a> - <a *ngIf="getConfigDownloadResultFormat('download_vo')" [href]="getUrl('votable')" target="_blank" class="btn btn-outline-primary" title="Download results in VO format"> + <a *ngIf="getConfigDownloadResultFormat('download_vo')" [href]="getUrl('votable')" (click)="click($event, getUrl('votable'), 'xml')" class="btn btn-outline-primary" title="Download results in VO format"> <i class="fas fa-file"></i> VOtable </a> @@ -41,7 +41,7 @@ <p>Download archive files just here:</p> </div> <div class="col"> - <a [href]="getUrlArchive()" class="btn btn-outline-primary" title="Download an archive with all files"> + <a [href]="getUrlArchive()" (click)="click($event, getUrlArchive(), 'zip')" class="btn btn-outline-primary" title="Download an archive with all files"> <i class="fas fa-archive"></i> Files archive </a> </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 10607d8459b2b922a870567a800cd031fd3d4578..b0498b150edd0e4fb3b223875692fd6063aac658 100644 --- a/client/src/app/instance/search/components/result/download.component.ts +++ b/client/src/app/instance/search/components/result/download.component.ts @@ -8,6 +8,7 @@ */ import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; import { Criterion, ConeSearch, criterionToString } from '../../../store/models'; import { Dataset } from 'src/app/metamodel/models'; @@ -34,7 +35,7 @@ export class DownloadComponent { @Input() sampRegistered: boolean; @Output() broadcast: EventEmitter<string> = new EventEmitter(); - constructor(private appConfig: AppConfigService) { } + constructor(private appConfig: AppConfigService, private http: HttpClient) { } isDownloadActivated(): boolean { const dataset = this.datasetList.find(dataset => dataset.name === this.datasetSelected); @@ -91,4 +92,17 @@ export class DownloadComponent { broadcastVotable(): void { this.broadcast.emit(this.getUrl('votable')); } + + click(event, href, extension) { + event.preventDefault(); + + this.http.get(href, {responseType: "blob"}).subscribe( + data => { + let downloadLink = document.createElement('a'); + downloadLink.href = window.URL.createObjectURL(data); + downloadLink.setAttribute('download', `${this.datasetSelected}.${extension}`); + downloadLink.click(); + } + ); + } } diff --git a/client/src/app/instance/search/components/summary.component.ts b/client/src/app/instance/search/components/summary.component.ts index 3e76c6275872536dca5f00257e2969ea611ad3b3..10170ce92de91aa238a7afd542a5e2ddc565fe4a 100644 --- a/client/src/app/instance/search/components/summary.component.ts +++ b/client/src/app/instance/search/components/summary.component.ts @@ -88,9 +88,7 @@ export class SummaryComponent { * @return Category[] */ getCategoryByFamilySortedByDisplay(idFamily: number): OutputCategory[] { - return this.outputCategoryList - .filter(category => category.id_output_family === idFamily) - //.sort(sortByDisplay); + return this.outputCategoryList.filter(category => category.id_output_family === idFamily) } /** @@ -102,8 +100,6 @@ export class SummaryComponent { */ getSelectedOutputByCategory(idCategory: number): Attribute[] { const outputListByCategory = this.attributeList.filter(attribute => attribute.id_output_category === idCategory); - return outputListByCategory - .filter(attribute => this.outputList.includes(attribute.id)) - .sort((a, b) => a.output_display - b.output_display); + return outputListByCategory.filter(attribute => this.outputList.includes(attribute.id)); } } diff --git a/client/src/app/instance/search/containers/criteria.component.html b/client/src/app/instance/search/containers/criteria.component.html index 8ff2d1d903ea462d493393f6efa68700811f4265..d21e4e515041a80ba8e2b672cabea8260dedde89 100644 --- a/client/src/app/instance/search/containers/criteria.component.html +++ b/client/src/app/instance/search/containers/criteria.component.html @@ -19,7 +19,7 @@ (retrieveCoordinates)="retrieveCoordinates($event)"> </app-cone-search-tab> <app-criteria-tabs - [attributeList]="attributeList | async" + [attributeList]="attributeList | async | sortByCriteriaDisplay" [criteriaFamilyList]="criteriaFamilyList | async" [criteriaList]="criteriaList | async" (addCriterion)="addCriterion($event)" @@ -32,7 +32,7 @@ [currentStep]="currentStep | async" [datasetSelected]="datasetSelected | async" [datasetList]="datasetList | async" - [attributeList]="attributeList | async" + [attributeList]="attributeList | async | sortByOutputDisplay" [criteriaFamilyList]="criteriaFamilyList | async" [outputFamilyList]="outputFamilyList | async" [outputCategoryList]="outputCategoryList | async" diff --git a/client/src/app/instance/search/containers/dataset.component.html b/client/src/app/instance/search/containers/dataset.component.html index 77107988152d7cafa0de1aee2c1d0172768d3afd..3b0dd7da561982ad3c9797ac4d436daf82e5c08a 100644 --- a/client/src/app/instance/search/containers/dataset.component.html +++ b/client/src/app/instance/search/containers/dataset.component.html @@ -30,7 +30,7 @@ [currentStep]="currentStep | async" [datasetSelected]="datasetSelected | async" [datasetList]="datasetList | async" - [attributeList]="attributeList | async" + [attributeList]="attributeList | async | sortByOutputDisplay" [criteriaFamilyList]="criteriaFamilyList | async" [outputFamilyList]="outputFamilyList | async" [outputCategoryList]="outputCategoryList | async" diff --git a/client/src/app/instance/search/containers/output.component.html b/client/src/app/instance/search/containers/output.component.html index db66948c6961e8d968487796e09c4723027f6337..8133aecab64ed999ecbcd3096ef522fe9783bac3 100644 --- a/client/src/app/instance/search/containers/output.component.html +++ b/client/src/app/instance/search/containers/output.component.html @@ -8,7 +8,7 @@ && (outputCategoryListIsLoaded | async)" class="row mt-4"> <div class="col-12 col-md-8 col-lg-9"> <app-output-tabs - [attributeList]="attributeList | async" + [attributeList]="attributeList | async | sortByOutputDisplay" [outputFamilyList]="outputFamilyList | async" [outputCategoryList]="outputCategoryList | async" [outputList]="outputList | async" @@ -22,7 +22,7 @@ [currentStep]="currentStep | async" [datasetSelected]="datasetSelected | async" [datasetList]="datasetList | async" - [attributeList]="attributeList | async" + [attributeList]="attributeList | async | sortByOutputDisplay" [criteriaFamilyList]="criteriaFamilyList | async" [outputFamilyList]="outputFamilyList | async" [outputCategoryList]="outputCategoryList | async" diff --git a/client/src/app/instance/search/containers/result.component.html b/client/src/app/instance/search/containers/result.component.html index d9b4fdbf2a8260fc9d97ecb02df3294d25a04e9f..052cf4ed64375604dfdd2b3e57adeb0509ff5205 100644 --- a/client/src/app/instance/search/containers/result.component.html +++ b/client/src/app/instance/search/containers/result.component.html @@ -38,7 +38,7 @@ <app-reminder [datasetSelected]="datasetSelected | async" [datasetList]="datasetList | async" - [attributeList]="attributeList | async" + [attributeList]="attributeList | async | sortByOutputDisplay" [criteriaFamilyList]="criteriaFamilyList | async" [outputFamilyList]="outputFamilyList | async" [outputCategoryList]="outputCategoryList | async" @@ -74,7 +74,7 @@ [datasetSelected]="datasetSelected | async" [instance]="instance | async" [datasetList]="datasetList | async" - [attributeList]="attributeList | async" + [attributeList]="attributeList | async | sortByOutputDisplay" [outputList]="outputList | async" [queryParams]="queryParams | async" [dataLength]="dataLength | async" diff --git a/client/src/app/instance/search/containers/result.component.ts b/client/src/app/instance/search/containers/result.component.ts index 82f65348ed3b1cd56878cb116aa9eaae9053fec9..f62ca538ed8b10b2a94be5d2b5e3778e92907a22 100644 --- a/client/src/app/instance/search/containers/result.component.ts +++ b/client/src/app/instance/search/containers/result.component.ts @@ -68,7 +68,7 @@ export class ResultComponent extends AbstractSearchComponent { if (!pristine) { Promise.resolve(null).then(() => this.store.dispatch(searchActions.retrieveDataLength())); } - }) + }); } sampRegister() { diff --git a/client/src/app/instance/search/pipes/index.ts b/client/src/app/instance/search/pipes/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..dd9b75775430c5435ec14b392378948d3f3f9629 --- /dev/null +++ b/client/src/app/instance/search/pipes/index.ts @@ -0,0 +1,16 @@ +/** + * 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 { SortByCriteriaDisplay } from './sort-by-criteria-display'; +import { SortByOutputDisplay } from './sort-by-output-display'; + +export const searchPipes = [ + SortByCriteriaDisplay, + SortByOutputDisplay +]; \ No newline at end of file diff --git a/client/src/app/instance/search/pipes/sort-by-criteria-display.ts b/client/src/app/instance/search/pipes/sort-by-criteria-display.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6c8bd4a8ce5b6830be220ed0f33c5a0807133b9 --- /dev/null +++ b/client/src/app/instance/search/pipes/sort-by-criteria-display.ts @@ -0,0 +1,19 @@ +/** + * 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 { Pipe, PipeTransform } from '@angular/core'; + +import { Attribute } from 'src/app/metamodel/models'; + +@Pipe({name: 'sortByCriteriaDisplay'}) +export class SortByCriteriaDisplay implements PipeTransform { + transform(attributeList: Attribute[]): Attribute[] { + return [...attributeList].sort((a: Attribute, b: Attribute) => a.criteria_display - b.criteria_display); + } +} diff --git a/client/src/app/instance/search/pipes/sort-by-output-display.ts b/client/src/app/instance/search/pipes/sort-by-output-display.ts new file mode 100644 index 0000000000000000000000000000000000000000..cfd308029821814d55f3a88641d1cf2ff3e06803 --- /dev/null +++ b/client/src/app/instance/search/pipes/sort-by-output-display.ts @@ -0,0 +1,19 @@ +/** + * 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 { Pipe, PipeTransform } from '@angular/core'; + +import { Attribute } from 'src/app/metamodel/models'; + +@Pipe({name: 'sortByOutputDisplay'}) +export class SortByOutputDisplay implements PipeTransform { + transform(attributeList: Attribute[]): Attribute[] { + return [...attributeList].sort((a: Attribute, b: Attribute) => a.output_display - b.output_display); + } +} diff --git a/client/src/app/instance/search/search.module.ts b/client/src/app/instance/search/search.module.ts index 63eef0ff7d627926da0f1dd73535176b9bd1a303..0d25c4b7aa49df21fdd55d7bdf27e4bf458d2402 100644 --- a/client/src/app/instance/search/search.module.ts +++ b/client/src/app/instance/search/search.module.ts @@ -13,6 +13,7 @@ import { SharedModule } from 'src/app/shared/shared.module'; import { SharedSearchModule } from '../shared-search/shared-search.module'; import { SearchRoutingModule, routedComponents } from './search-routing.module'; import { dummiesComponents } from './components'; +import { searchPipes } from './pipes'; @NgModule({ imports: [ @@ -22,7 +23,8 @@ import { dummiesComponents } from './components'; ], declarations: [ routedComponents, - dummiesComponents + dummiesComponents, + searchPipes ] }) export class SearchModule { } diff --git a/client/src/app/instance/shared-search/components/cone-search/cone-search.component.ts b/client/src/app/instance/shared-search/components/cone-search/cone-search.component.ts index 59de49ada64fd52e4fcd09720480a1bb8d2daf7d..0bd90e12c4ef706ddd60ccb4bf6a151e8ea2eb6e 100644 --- a/client/src/app/instance/shared-search/components/cone-search/cone-search.component.ts +++ b/client/src/app/instance/shared-search/components/cone-search/cone-search.component.ts @@ -26,8 +26,6 @@ export class ConeSearchComponent implements OnChanges { @Input() resolver: Resolver; @Input() resolverIsLoading: boolean; @Input() resolverIsLoaded: boolean; - @Output() addConeSearch: EventEmitter<ConeSearch> = new EventEmitter(); - @Output() deleteConeSearch: EventEmitter<{ }> = new EventEmitter(); @Output() retrieveCoordinates: EventEmitter<string> = new EventEmitter(); public form = new FormGroup({ diff --git a/client/src/app/instance/shared-search/components/datatable/datatable.component.html b/client/src/app/instance/shared-search/components/datatable/datatable.component.html index 7b7ee23a3405f901e1183f30f59c9dbbbfeceefc..ecb7f5726a7055b7592d8fc099ca53845d04bb19 100644 --- a/client/src/app/instance/shared-search/components/datatable/datatable.component.html +++ b/client/src/app/instance/shared-search/components/datatable/datatable.component.html @@ -40,7 +40,7 @@ </button> </td> <td *ngFor="let attribute of getOutputList()" class="align-middle"> - <div [ngSwitch]="attribute.renderer"> + <div *ngIf="datum[attribute.label]" [ngSwitch]="attribute.renderer"> <div *ngSwitchCase="'detail'"> <app-detail-renderer [value]="datum[attribute.label]" @@ -61,6 +61,7 @@ <app-download-renderer [value]="datum[attribute.label]" [datasetName]="dataset.name" + [datasetPublic]="dataset.public" [config]="getRendererConfig(attribute)"> </app-download-renderer> </div> diff --git a/client/src/app/instance/shared-search/components/datatable/datatable.component.ts b/client/src/app/instance/shared-search/components/datatable/datatable.component.ts index 1e90851ca9be53f1a232c612002eb5a8ae64981f..08038733980a47f874e9665701321a37fb373204 100644 --- a/client/src/app/instance/shared-search/components/datatable/datatable.component.ts +++ b/client/src/app/instance/shared-search/components/datatable/datatable.component.ts @@ -85,8 +85,7 @@ export class DatatableComponent implements OnInit { */ getOutputList(): Attribute[] { return this.attributeList - .filter(a => this.outputList.includes(a.id)) - .sort((a, b) => a.output_display - b.output_display); + .filter(a => this.outputList.includes(a.id)); } /** diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.html b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.html index aef5f0ea9282018d805f76a7cd788e1921333b61..ad2ae4dd7e841767556c40e6e838c75b69eb9608 100644 --- a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.html +++ b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.html @@ -1,4 +1,4 @@ -<a [href]="getHref()" [ngClass]="{'btn btn-outline-primary btn-sm': (config.display=='text-button' || config.display=='icon-button' || config.display=='icon-text-btn')}"> +<a [href]="getHref()" (click)="click($event)" [ngClass]="{'btn btn-outline-primary btn-sm': (config.display=='text-button' || config.display=='icon-button' || config.display=='icon-text-btn')}"> <span *ngIf="config.display === 'icon-button' || config.display === 'icon-text-btn'" class="{{config.icon}}"></span> <span *ngIf="config.display === 'icon-text-btn'"> </span> <span *ngIf="config.display !== 'icon-button'">{{ getText() }}</span> diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts index 19bcc38572ede27f043ee814f2f330d76eabeb25..56475b8e0d2615e254096a12f551caaa5338b4b9 100644 --- a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts +++ b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts @@ -8,6 +8,7 @@ */ import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; import { DownloadRendererConfig } from 'src/app/metamodel/models/renderers/download-renderer-config.model'; import { getHost } from 'src/app/shared/utils'; @@ -25,9 +26,10 @@ import { AppConfigService } from 'src/app/app-config.service'; export class DownloadRendererComponent { @Input() value: string; @Input() datasetName: string; + @Input() datasetPublic: boolean; @Input() config: DownloadRendererConfig; - constructor(private appConfig: AppConfigService) { } + constructor(private appConfig: AppConfigService, private http: HttpClient) { } /** * Returns link href. @@ -46,4 +48,20 @@ export class DownloadRendererComponent { getText(): string { return this.config.text.replace('$value', this.value.toString()); } + + click(event) { + event.preventDefault(); + + const href = this.getHref(); + this.http.get(href, {responseType: "blob"}).subscribe( + data => { + const filename = href.substring(href.lastIndexOf('/') + 1); + + let downloadLink = document.createElement('a'); + downloadLink.href = window.URL.createObjectURL(data); + downloadLink.setAttribute('download', filename); + downloadLink.click(); + } + ); + } } diff --git a/client/src/app/instance/shared-search/detail/components/object-data.component.ts b/client/src/app/instance/shared-search/detail/components/object-data.component.ts index 16de054ffd8348a0701382356e74f9412523540e..943dab66734787e4e0d44e4933d0ade6e51c7b0f 100644 --- a/client/src/app/instance/shared-search/detail/components/object-data.component.ts +++ b/client/src/app/instance/shared-search/detail/components/object-data.component.ts @@ -40,8 +40,7 @@ export class ObjectDataComponent { */ getCategoryByFamilySortedByDisplay(idFamily: number): OutputCategory[] { return this.outputCategoryList - .filter(category => category.id_output_family === idFamily) - //.sort(sortByDisplay); + .filter(category => category.id_output_family === idFamily); } /** @@ -54,8 +53,7 @@ export class ObjectDataComponent { getAttributesVisibleByCategory(idCategory: number): Attribute[] { return this.attributeList .filter(a => a.detail) - .filter(a => a.id_output_category === idCategory) - .sort((a, b) => a.display_detail - b.display_detail); + .filter(a => a.id_output_category === idCategory); } /** @@ -64,9 +62,7 @@ export class ObjectDataComponent { * @return Attribute[] */ getAttributesVisible(): Attribute[] { - return this.attributeList - .filter(a => a.detail) - .sort((a, b) => a.display_detail - b.display_detail); + return this.attributeList.filter(a => a.detail); } /** diff --git a/client/src/app/instance/shared-search/detail/containers/detail.component.html b/client/src/app/instance/shared-search/detail/containers/detail.component.html index 2899b8cf78fc9f670a81366d3efd8b049e074bc6..a17e8166574a26436d058f1db8718f1aad2c5a03 100644 --- a/client/src/app/instance/shared-search/detail/containers/detail.component.html +++ b/client/src/app/instance/shared-search/detail/containers/detail.component.html @@ -16,7 +16,7 @@ [datasetSelected]="datasetSelected | async" [outputFamilyList]="outputFamilyList | async" [outputCategoryList]="outputCategoryList | async" - [attributeList]="attributeList | async" + [attributeList]="attributeList | async | sortByDetailDisplay" [object]="object | async" [spectraCSV]="spectraCSV | async" [spectraIsLoading]="spectraIsLoading | async" @@ -27,7 +27,7 @@ [datasetSelected]="datasetSelected | async" [outputFamilyList]="outputFamilyList | async" [outputCategoryList]="outputCategoryList | async" - [attributeList]="attributeList | async" + [attributeList]="attributeList | async | sortByDetailDisplay" [object]="object | async"> </app-default-object> </div> diff --git a/client/src/app/instance/shared-search/detail/detail.module.ts b/client/src/app/instance/shared-search/detail/detail.module.ts index d9d97c4f5c9ae69ff1a5b6a85b6b71836f2f1767..af987eed26912d826161994901356c3edf6cd762 100644 --- a/client/src/app/instance/shared-search/detail/detail.module.ts +++ b/client/src/app/instance/shared-search/detail/detail.module.ts @@ -12,6 +12,7 @@ import { NgModule } from '@angular/core'; import { SharedModule } from 'src/app/shared/shared.module'; import { DetailComponent } from './containers/detail.component'; import { dummiesComponents } from './components'; +import { detailPipes } from './pipes'; @NgModule({ imports: [ @@ -19,7 +20,8 @@ import { dummiesComponents } from './components'; ], declarations: [ DetailComponent, - dummiesComponents + dummiesComponents, + detailPipes ] }) export class DetailModule { } diff --git a/client/src/app/instance/shared-search/detail/pipes/index.ts b/client/src/app/instance/shared-search/detail/pipes/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..a42bb61221d4720af663e83ca2f4413405bc688d --- /dev/null +++ b/client/src/app/instance/shared-search/detail/pipes/index.ts @@ -0,0 +1,5 @@ +import { SortByDetailDisplay } from './sort-by-detail-display'; + +export const detailPipes = [ + SortByDetailDisplay +]; diff --git a/client/src/app/instance/shared-search/detail/pipes/sort-by-detail-display.ts b/client/src/app/instance/shared-search/detail/pipes/sort-by-detail-display.ts new file mode 100644 index 0000000000000000000000000000000000000000..dd17a7ca46d072c91e36aa534811d4ae89a2ada4 --- /dev/null +++ b/client/src/app/instance/shared-search/detail/pipes/sort-by-detail-display.ts @@ -0,0 +1,19 @@ +/** + * 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 { Pipe, PipeTransform } from '@angular/core'; + +import { Attribute } from 'src/app/metamodel/models'; + +@Pipe({name: 'sortByDetailDisplay'}) +export class SortByDetailDisplay implements PipeTransform { + transform(attributeList: Attribute[]): Attribute[] { + return [...attributeList].sort((a: Attribute, b: Attribute) => a.display_detail - b.display_detail); + } +} diff --git a/client/src/app/instance/store/actions/search-multiple.actions.ts b/client/src/app/instance/store/actions/search-multiple.actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..87d20074517eeb51f87b1895a8c81f6d9a5161a8 --- /dev/null +++ b/client/src/app/instance/store/actions/search-multiple.actions.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 { createAction, props } from '@ngrx/store'; + +import { SearchMultipleDatasetLength, SearchMultipleDatasetData } from '../models'; + +export const initSearch = createAction('[Search Multiple] Init Search'); +export const restartSearch = createAction('[Search Multiple] Restart Search'); +export const markAsDirty = createAction('[Search Multiple] Mark As Dirty'); +export const changeStep = createAction('[Search Multiple] Change Step', props<{ step: string }>()); +export const checkDatasets = createAction('[Search Multiple] Check Datasets'); +export const checkResult = createAction('[Search Multiple] Check Result'); +export const updateSelectedDatasets = createAction('[Search Multiple] Update Selected Datasets', props<{ selectedDatasets: string[] }>()); +export const retrieveDataLength = createAction('[Search Multiple] Retrieve Data Length'); +export const retrieveDataLengthSuccess = createAction('[Search Multiple] Retrieve Data Length Success', props<{ dataLength: SearchMultipleDatasetLength[] }>()); +export const retrieveDataLengthFail = createAction('[Search Multiple] Retrieve Data Length Fail'); +export const retrieveData = createAction('[Search Multiple] Retrieve Data'); +export const retrieveDataSuccess = createAction('[Search Multiple] Retrieve Data Success', props<{ data: SearchMultipleDatasetData[] }>()); +export const retrieveDataFail = createAction('[Search Multiple] Retrieve Data Fail'); diff --git a/client/src/app/instance/store/effects/index.ts b/client/src/app/instance/store/effects/index.ts index 132bb177b9768dd206195f68ed2d2eef1ca6a8fd..582dd51102f0f5b76cdf2db4ff4fd0e3e36307b6 100644 --- a/client/src/app/instance/store/effects/index.ts +++ b/client/src/app/instance/store/effects/index.ts @@ -1,11 +1,13 @@ import { SampEffects } from './samp.effects'; import { SearchEffects } from './search.effects'; +import { SearchMultipleEffects } from './search-multiple.effects'; import { ConeSearchEffects } from './cone-search.effects'; import { DetailEffects } from './detail.effects'; export const instanceEffects = [ SampEffects, SearchEffects, + SearchMultipleEffects, ConeSearchEffects, DetailEffects ]; diff --git a/client/src/app/instance/store/effects/search-multiple.effects.ts b/client/src/app/instance/store/effects/search-multiple.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..84382d44732bfb1c719f7d0cbaa96e4326c693a3 --- /dev/null +++ b/client/src/app/instance/store/effects/search-multiple.effects.ts @@ -0,0 +1,132 @@ +/** + * 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, Action } from '@ngrx/store'; +import { forkJoin, of } from 'rxjs'; +import { map, tap, mergeMap, catchError } from 'rxjs/operators'; +import { ToastrService } from 'ngx-toastr'; + +import { SearchMultipleDatasetLength } from '../models'; +import { SearchService } from '../services/search.service'; +import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; +import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector'; +import * as searchMultipleActions from '../actions/search-multiple.actions'; +import * as searchMultipleSelector from '../selectors/search-multiple.selector'; +import * as coneSearchActions from '../actions/cone-search.actions'; +import * as coneSearchSelector from '../selectors/cone-search.selector'; + +@Injectable() +export class SearchMultipleEffects { + initSearch$ = createEffect(() => + this.actions$.pipe( + ofType(searchMultipleActions.initSearch), + concatLatestFrom(() => [ + this.store.select(searchMultipleSelector.selectPristine), + this.store.select(coneSearchSelector.selectConeSearchByRoute), + this.store.select(searchMultipleSelector.selectSelectedDatasetsByRoute), + this.store.select(instanceSelector.selectInstanceByRouteName), + this.store.select(datasetSelector.selectAllConeSearchDatasets) + ]), + mergeMap(([action, pristine, coneSearchByRoute, selectedDatasetsByRoute, instance, datasetList]) => { + if (!pristine && !coneSearchByRoute) { + // Restart search + return [ + coneSearchActions.deleteConeSearch(), + searchMultipleActions.restartSearch() + ]; + } + + if (!pristine) { + // Default form parameters already loaded or no dataset selected + return of({ type: '[No Action] Load Default Form Parameters' }); + } + + let actions: Action[] = [ + searchMultipleActions.markAsDirty() + ]; + + // Update cone search + if (coneSearchByRoute) { + const params = coneSearchByRoute.split(':'); + const coneSearch = { + ra: +params[0], + dec: +params[1], + radius: +params[2] + }; + actions.push(coneSearchActions.addConeSearch({ coneSearch })); + } + + // Update selected datasets + if (selectedDatasetsByRoute) { + // Build output list with the URL query parameters (a) + const selectedDatasets = selectedDatasetsByRoute.split(';'); + actions.push( + searchMultipleActions.updateSelectedDatasets({ selectedDatasets }), + searchMultipleActions.checkDatasets() + ); + } else if (instance.config.search.search_multiple_all_datasets_selected) { + const selectedDatasets = datasetList.map(dataset => dataset.name); + actions.push( + searchMultipleActions.updateSelectedDatasets({ selectedDatasets }) + ); + } + + // Returns actions and mark the form as dirty + return actions; + }) + ) + ); + + restartSearch$ = createEffect(() => + this.actions$.pipe( + ofType(searchMultipleActions.restartSearch), + map(() => searchMultipleActions.initSearch()) + ) + ); + + retrieveDataLength$ = createEffect(() => + this.actions$.pipe( + ofType(searchMultipleActions.retrieveDataLength), + concatLatestFrom(() => [ + this.store.select(searchMultipleSelector.selectSelectedDatasets), + this.store.select(coneSearchSelector.selectConeSearch) + ]), + mergeMap(([action, selectedDatasets, coneSearch]) => { + const queries = selectedDatasets.map(datasetName => this.searchService.retrieveDataLength( + `${datasetName}?a=count&cs=${coneSearch.ra}:${coneSearch.dec}:${coneSearch.radius}` + ).pipe( + map((response: { nb: number }[]) => ({ datasetName, length: response[0].nb })) + )); + + return forkJoin(queries) + .pipe( + map((response: SearchMultipleDatasetLength[]) => searchMultipleActions.retrieveDataLengthSuccess({ dataLength: response })), + catchError(() => of(searchMultipleActions.retrieveDataLengthFail())) + ) + }) + ) + ); + + retrieveDataLengthFail$ = createEffect(() => + this.actions$.pipe( + ofType(searchMultipleActions.retrieveDataLengthFail), + tap(() => this.toastr.error('Loading Failed', 'The search multiple data length loading failed')) + ), { dispatch: false} + ); + + constructor( + private actions$: Actions, + private searchService: SearchService, + private store: Store<{ }>, + private toastr: ToastrService + ) {} +} diff --git a/client/src/app/instance/store/models/index.ts b/client/src/app/instance/store/models/index.ts index c471d6929ad3c505f8a5a9386d3d6f716d49e750..962eede1ad19a550541f007ee3155525a696c59c 100644 --- a/client/src/app/instance/store/models/index.ts +++ b/client/src/app/instance/store/models/index.ts @@ -1,6 +1,9 @@ export * from './criterion.model'; export * from './search-query-params.model'; +export * from './search-multiple-query-params.model'; export * from './criterion'; export * from './pagination.model'; export * from './cone-search.model'; export * from './resolver.model'; +export * from './search-multiple-dataset-length'; +export * from './search-multiple-dataset-data'; diff --git a/client/src/app/instance/store/models/search-multiple-dataset-data.ts b/client/src/app/instance/store/models/search-multiple-dataset-data.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa25ed88176093d7840cdd984c476aed52b25ed3 --- /dev/null +++ b/client/src/app/instance/store/models/search-multiple-dataset-data.ts @@ -0,0 +1,13 @@ +/** + * 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 SearchMultipleDatasetData { + datasetName: string; + data: any[]; +} diff --git a/client/src/app/instance/store/models/search-multiple-dataset-length.ts b/client/src/app/instance/store/models/search-multiple-dataset-length.ts new file mode 100644 index 0000000000000000000000000000000000000000..75961a0d5c68acd8878099d32715c9a4c9143242 --- /dev/null +++ b/client/src/app/instance/store/models/search-multiple-dataset-length.ts @@ -0,0 +1,13 @@ +/** + * 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 SearchMultipleDatasetLength { + datasetName: string; + length: number; +} diff --git a/client/src/app/instance/store/models/search-multiple-query-params.model.ts b/client/src/app/instance/store/models/search-multiple-query-params.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e4cd67b1047b0288bf8fcfabe5c895f41a1713a --- /dev/null +++ b/client/src/app/instance/store/models/search-multiple-query-params.model.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. + */ + +/** + * Interface for search multiple query parameters. + * + * @interface SearchMultipleQueryParams + */ +export interface SearchMultipleQueryParams { + cs?: string; + d?: string; +} diff --git a/client/src/app/instance/store/reducers/search-multiple.reducer.ts b/client/src/app/instance/store/reducers/search-multiple.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd73fbba1df15c2853d55d21157e8a7f30fb1dbb --- /dev/null +++ b/client/src/app/instance/store/reducers/search-multiple.reducer.ts @@ -0,0 +1,114 @@ +/** + * 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 { SearchMultipleDatasetLength, SearchMultipleDatasetData } from '../models'; +import * as searchMultipleActions from '../actions/search-multiple.actions'; + +export interface State { + pristine: boolean; + currentStep: string; + positionStepChecked: boolean; + datasetsStepChecked: boolean; + resultStepChecked: boolean; + selectedDatasets: string[]; + dataLengthIsLoading: boolean; + dataLengthIsLoaded: boolean; + dataLength: SearchMultipleDatasetLength[]; + dataIsLoading: boolean; + dataIsLoaded: boolean; + data: SearchMultipleDatasetData[]; +} + +export const initialState: State = { + pristine: true, + currentStep: null, + positionStepChecked: false, + datasetsStepChecked: false, + resultStepChecked: false, + selectedDatasets: [], + dataLengthIsLoading: false, + dataLengthIsLoaded: false, + dataLength: [], + dataIsLoading: false, + dataIsLoaded: false, + data: [] +}; + +export const searchMultipleReducer = createReducer( + initialState, + on(searchMultipleActions.restartSearch, () => ({ + ...initialState, + currentStep: 'position' + })), + on(searchMultipleActions.changeStep, (state, { step }) => ({ + ...state, + currentStep: step + })), + on(searchMultipleActions.markAsDirty, state => ({ + ...state, + pristine: false + })), + on(searchMultipleActions.checkDatasets, state => ({ + ...state, + datasetsStepChecked: true + })), + on(searchMultipleActions.checkResult, state => ({ + ...state, + resultStepChecked: true + })), + on(searchMultipleActions.updateSelectedDatasets, (state, { selectedDatasets }) => ({ + ...state, + selectedDatasets + })), + on(searchMultipleActions.retrieveDataLength, state => ({ + ...state, + dataLengthIsLoading: true, + dataLengthIsLoaded: false + })), + on(searchMultipleActions.retrieveDataLengthSuccess, (state, { dataLength }) => ({ + ...state, + dataLength, + dataLengthIsLoading: false, + dataLengthIsLoaded: true + })), + on(searchMultipleActions.retrieveDataLengthFail, state => ({ + ...state, + dataLengthIsLoading: false + })), + on(searchMultipleActions.retrieveData, state => ({ + ...state, + dataIsLoading: true, + dataIsLoaded: false + })), + on(searchMultipleActions.retrieveDataSuccess, (state, { data }) => ({ + ...state, + data, + dataIsLoading: false, + dataIsLoaded: true + })), + on(searchMultipleActions.retrieveDataFail, state => ({ + ...state, + dataIsLoading: false + })) +); + +export const selectPristine = (state: State) => state.pristine; +export const selectCurrentStep = (state: State) => state.currentStep; +export const selectPositionStepChecked = (state: State) => state.positionStepChecked; +export const selectDatasetsStepChecked = (state: State) => state.datasetsStepChecked; +export const selectResultStepChecked = (state: State) => state.resultStepChecked; +export const selectSelectedDatasets = (state: State) => state.selectedDatasets; +export const selectDataLengthIsLoading = (state: State) => state.dataLengthIsLoading; +export const selectDataLengthIsLoaded = (state: State) => state.dataLengthIsLoaded; +export const selectDataLength = (state: State) => state.dataLength; +export const selectDataIsLoading = (state: State) => state.dataIsLoading; +export const selectDataIsLoaded = (state: State) => state.dataIsLoaded; +export const selectData = (state: State) => state.data; diff --git a/client/src/app/instance/store/selectors/search-multiple.selector.ts b/client/src/app/instance/store/selectors/search-multiple.selector.ts new file mode 100644 index 0000000000000000000000000000000000000000..61ae2e99501760bef036054717ac11ad5408ad42 --- /dev/null +++ b/client/src/app/instance/store/selectors/search-multiple.selector.ts @@ -0,0 +1,108 @@ +/** + * 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 { createSelector } from '@ngrx/store'; + +import { SearchMultipleQueryParams, ConeSearch } from '../models'; +import * as reducer from '../../instance.reducer'; +import * as fromSearchMultiple from '../reducers/search-multiple.reducer'; +import * as coneSearchSelector from './cone-search.selector'; + +export const selectSearchMultipleState = createSelector( + reducer.getInstanceState, + (state: reducer.State) => state.searchMultiple +); + +export const selectPristine = createSelector( + selectSearchMultipleState, + fromSearchMultiple.selectPristine +); + +export const selectCurrentStep = createSelector( + selectSearchMultipleState, + fromSearchMultiple.selectCurrentStep +); + +export const selectPositionStepChecked = createSelector( + selectSearchMultipleState, + fromSearchMultiple.selectPositionStepChecked +); + +export const selectDatasetsStepChecked = createSelector( + selectSearchMultipleState, + fromSearchMultiple.selectDatasetsStepChecked +); + +export const selectResultStepChecked = createSelector( + selectSearchMultipleState, + fromSearchMultiple.selectResultStepChecked +); + +export const selectSelectedDatasets = createSelector( + selectSearchMultipleState, + fromSearchMultiple.selectSelectedDatasets +); + +export const selectDataLengthIsLoading = createSelector( + selectSearchMultipleState, + fromSearchMultiple.selectDataLengthIsLoading +); + +export const selectDataLengthIsLoaded = createSelector( + selectSearchMultipleState, + fromSearchMultiple.selectDataLengthIsLoaded +); + +export const selectDataLength = createSelector( + selectSearchMultipleState, + fromSearchMultiple.selectDataLength +); + +export const selectDataIsLoading = createSelector( + selectSearchMultipleState, + fromSearchMultiple.selectDataIsLoading +); + +export const selectDataIsLoaded = createSelector( + selectSearchMultipleState, + fromSearchMultiple.selectDataIsLoaded +); + +export const selectData = createSelector( + selectSearchMultipleState, + fromSearchMultiple.selectData +); + +export const selectQueryParams = createSelector( + coneSearchSelector.selectConeSearch, + selectSelectedDatasets, + ( + coneSearch: ConeSearch, + selectedDatasets: string[]) => { + let queryParams: SearchMultipleQueryParams = { }; + if (coneSearch) { + queryParams = { + ...queryParams, + cs: coneSearch.ra + ':' + coneSearch.dec + ':' + coneSearch.radius + }; + } + if (selectedDatasets.length > 0) { + queryParams = { + ...queryParams, + d: selectedDatasets.join(';') + }; + } + return queryParams; + } +); + +export const selectSelectedDatasetsByRoute = createSelector( + reducer.selectRouterState, + router => router.state.queryParams.d as string +); diff --git a/client/src/app/metamodel/effects/database.effects.ts b/client/src/app/metamodel/effects/database.effects.ts index 968712fcd7bee8b3e00bab06778feb756528d544..39c20f7e43a15f87707f3d31cfbd924ba21ffbf7 100644 --- a/client/src/app/metamodel/effects/database.effects.ts +++ b/client/src/app/metamodel/effects/database.effects.ts @@ -48,7 +48,7 @@ export class DatabaseEffects { this.actions$.pipe( ofType(databaseActions.addDatabaseSuccess), tap(() => { - this.router.navigate(['/admin/survey/database-list']); + this.router.navigate(['/admin/database-list']); this.toastr.success('Database successfully added', 'The new database was added into the database') }) ), { dispatch: false} @@ -77,7 +77,7 @@ export class DatabaseEffects { this.actions$.pipe( ofType(databaseActions.editDatabaseSuccess), tap(() => { - this.router.navigate(['/admin/survey/database-list']); + this.router.navigate(['/admin/database-list']); this.toastr.success('Database successfully edited', 'The existing database has been edited into the database') }) ), { dispatch: false} diff --git a/client/src/app/metamodel/effects/survey.effects.ts b/client/src/app/metamodel/effects/survey.effects.ts index 7d876b5ac2fa0d0f8d87b9b13483e732daddbd76..fae21756b32a5bee17d13544db6304a07707e25b 100644 --- a/client/src/app/metamodel/effects/survey.effects.ts +++ b/client/src/app/metamodel/effects/survey.effects.ts @@ -48,7 +48,7 @@ export class SurveyEffects { this.actions$.pipe( ofType(surveyActions.addSurveySuccess), tap(() => { - this.router.navigate(['/admin/survey/survey-list']); + this.router.navigate(['/admin/survey-list']); this.toastr.success('Survey successfully added', 'The new survey was added into the database') }) ), { dispatch: false} @@ -77,7 +77,7 @@ export class SurveyEffects { this.actions$.pipe( ofType(surveyActions.editSurveySuccess), tap(() => { - this.router.navigate(['/admin/survey/survey-list']); + this.router.navigate(['/admin/survey-list']); this.toastr.success('Survey successfully edited', 'The existing survey has been edited into the database') }) ), { dispatch: false} diff --git a/client/src/app/metamodel/models/dataset.model.ts b/client/src/app/metamodel/models/dataset.model.ts index 03221eecfc307180f9416fc5c911c55b60e0c0cf..22e73074318ab24c91fdb32836dd9c3834c8a1ea 100644 --- a/client/src/app/metamodel/models/dataset.model.ts +++ b/client/src/app/metamodel/models/dataset.model.ts @@ -17,8 +17,13 @@ export interface Dataset { survey_name: string; id_dataset_family: number; public: boolean; + full_data_path: string; config: { images: any[], + survey: { + survey_enabled: boolean; + survey_label: string; + }, cone_search: { cone_search_enabled: boolean; cone_search_opened: boolean; diff --git a/client/src/app/metamodel/models/instance.model.ts b/client/src/app/metamodel/models/instance.model.ts index 006bdc1d3d058f9280a3dff2af46f549d3ce2515..c50e7bc234b97659b9b950b118669774f02ee582 100644 --- a/client/src/app/metamodel/models/instance.model.ts +++ b/client/src/app/metamodel/models/instance.model.ts @@ -10,18 +10,31 @@ export interface Instance { name: string; label: string; - client_url: string; + data_path: string; config: { design: { - design_color: string + design_color: string; + design_background_color: string; + design_logo: string; + design_favicon: string; + }; + home: { + home_component: string; + home_config: { + home_component_text: string; + home_component_logo: string; + }; }; search: { search_by_criteria_allowed: boolean; + search_by_criteria_label: string; search_multiple_allowed: boolean; + search_multiple_label: string; search_multiple_all_datasets_selected: boolean; }; documentation: { documentation_allowed: boolean; + documentation_label: string; }; }; nb_dataset_families: number; diff --git a/client/src/app/metamodel/reducers/criteria-family.reducer.ts b/client/src/app/metamodel/reducers/criteria-family.reducer.ts index 6e153b79fbc7e84d420a323e75bc956468fa7eed..76ccaae48cbaa1e8d6b28da6f878b495756734d6 100644 --- a/client/src/app/metamodel/reducers/criteria-family.reducer.ts +++ b/client/src/app/metamodel/reducers/criteria-family.reducer.ts @@ -18,7 +18,10 @@ export interface State extends EntityState<CriteriaFamily> { criteriaFamilyListIsLoaded: boolean; } -export const adapter: EntityAdapter<CriteriaFamily> = createEntityAdapter<CriteriaFamily>(); +export const adapter: EntityAdapter<CriteriaFamily> = createEntityAdapter<CriteriaFamily>({ + selectId: (criteriaFamily: CriteriaFamily) => criteriaFamily.id, + sortComparer: (a: CriteriaFamily, b: CriteriaFamily) => a.display - b.display +}); export const initialState: State = adapter.getInitialState({ criteriaFamilyListIsLoading: false, diff --git a/client/src/app/metamodel/reducers/dataset-family.reducer.ts b/client/src/app/metamodel/reducers/dataset-family.reducer.ts index dcbb9101dc1634f8283d58fab399dda6070a7367..923f88374e7b4211d4dd156554d7fa239bce8aa8 100644 --- a/client/src/app/metamodel/reducers/dataset-family.reducer.ts +++ b/client/src/app/metamodel/reducers/dataset-family.reducer.ts @@ -18,7 +18,10 @@ export interface State extends EntityState<DatasetFamily> { datasetFamilyListIsLoaded: boolean; } -export const adapter: EntityAdapter<DatasetFamily> = createEntityAdapter<DatasetFamily>(); +export const adapter: EntityAdapter<DatasetFamily> = createEntityAdapter<DatasetFamily>({ + selectId: (datasetFamily: DatasetFamily) => datasetFamily.id, + sortComparer: (a: DatasetFamily, b: DatasetFamily) => a.display - b.display +}); export const initialState: State = adapter.getInitialState({ datasetFamilyListIsLoading: false, diff --git a/client/src/app/metamodel/reducers/dataset.reducer.ts b/client/src/app/metamodel/reducers/dataset.reducer.ts index 82ba8ca599df9f7a99946e20eaaa279756eb4f43..becb9691e69c08353c027083fad1b096dcc8f522 100644 --- a/client/src/app/metamodel/reducers/dataset.reducer.ts +++ b/client/src/app/metamodel/reducers/dataset.reducer.ts @@ -20,7 +20,7 @@ export interface State extends EntityState<Dataset> { export const adapter: EntityAdapter<Dataset> = createEntityAdapter<Dataset>({ selectId: (dataset: Dataset) => dataset.name, - sortComparer: (a: Dataset, b: Dataset) => a.name.localeCompare(b.name) + sortComparer: (a: Dataset, b: Dataset) => a.display - b.display }); export const initialState: State = adapter.getInitialState({ diff --git a/client/src/app/metamodel/reducers/output-category.reducer.ts b/client/src/app/metamodel/reducers/output-category.reducer.ts index 11ef8e7b16f781292d0dd99caee4af8d0f2139aa..ddbed47415434fee7336a5e45078c8557d1b956e 100644 --- a/client/src/app/metamodel/reducers/output-category.reducer.ts +++ b/client/src/app/metamodel/reducers/output-category.reducer.ts @@ -18,7 +18,10 @@ export interface State extends EntityState<OutputCategory> { outputCategoryListIsLoaded: boolean; } -export const adapter: EntityAdapter<OutputCategory> = createEntityAdapter<OutputCategory>(); +export const adapter: EntityAdapter<OutputCategory> = createEntityAdapter<OutputCategory>({ + selectId: (outputCategory: OutputCategory) => outputCategory.id, + sortComparer: (a: OutputCategory, b: OutputCategory) => a.display - b.display +}); export const initialState: State = adapter.getInitialState({ outputCategoryListIsLoading: false, diff --git a/client/src/app/metamodel/reducers/output-family.reducer.ts b/client/src/app/metamodel/reducers/output-family.reducer.ts index ab02f1e9ecffafaf1675c5ed7ba015c8537bf79b..cb7c5f6b38320c69a2542149313eaae47467d41a 100644 --- a/client/src/app/metamodel/reducers/output-family.reducer.ts +++ b/client/src/app/metamodel/reducers/output-family.reducer.ts @@ -18,7 +18,10 @@ export interface State extends EntityState<OutputFamily> { outputFamilyListIsLoaded: boolean; } -export const adapter: EntityAdapter<OutputFamily> = createEntityAdapter<OutputFamily>(); +export const adapter: EntityAdapter<OutputFamily> = createEntityAdapter<OutputFamily>({ + selectId: (outputFamily: OutputFamily) => outputFamily.id, + sortComparer: (a: OutputFamily, b: OutputFamily) => a.display - b.display +}); export const initialState: State = adapter.getInitialState({ outputFamilyListIsLoading: false, diff --git a/client/src/app/metamodel/reducers/select-option.reducer.ts b/client/src/app/metamodel/reducers/select-option.reducer.ts index 13690b294c29adcfe1be595b218b60159f152c6f..f63ffbd5c6f5c2cdc84386301784731bd29fb681 100644 --- a/client/src/app/metamodel/reducers/select-option.reducer.ts +++ b/client/src/app/metamodel/reducers/select-option.reducer.ts @@ -18,7 +18,10 @@ export interface State extends EntityState<SelectOption> { selectOptionListIsLoaded: boolean; } -export const adapter: EntityAdapter<SelectOption> = createEntityAdapter<SelectOption>(); +export const adapter: EntityAdapter<SelectOption> = createEntityAdapter<SelectOption>({ + selectId: (selectOption: SelectOption) => selectOption.id, + sortComparer: (a: SelectOption, b: SelectOption) => a.display - b.display +}); export const initialState: State = adapter.getInitialState({ selectOptionListIsLoading: false, diff --git a/client/src/app/metamodel/selectors/dataset.selector.ts b/client/src/app/metamodel/selectors/dataset.selector.ts index 71294e9175bcb756fcbef983db7288edc3a18da3..d456b9e12640f2340cb11dd66d80c6edea60fb35 100644 --- a/client/src/app/metamodel/selectors/dataset.selector.ts +++ b/client/src/app/metamodel/selectors/dataset.selector.ts @@ -57,3 +57,8 @@ export const selectDatasetNameByRoute = createSelector( reducer.selectRouterState, router => router.state.params.dname as string ); + +export const selectAllConeSearchDatasets = createSelector( + selectAllDatasets, + datasetList => datasetList.filter(dataset => dataset.config.cone_search.cone_search_enabled) +); \ No newline at end of file diff --git a/client/src/app/metamodel/services/root-directory.service.ts b/client/src/app/metamodel/services/root-directory.service.ts index d173f00d10b99e3872109fac4823c225a2a5a674..56da1e927f274fa55c0195dd792b2ddfcf3a7a5e 100644 --- a/client/src/app/metamodel/services/root-directory.service.ts +++ b/client/src/app/metamodel/services/root-directory.service.ts @@ -20,6 +20,6 @@ export class RootDirectoryService { constructor(private http: HttpClient, private config: AppConfigService) { } retrieveRootDirectory(path: string): Observable<FileInfo[]> { - return this.http.get<FileInfo[]>(this.config.apiUrl + '/file-explorer/' + path); + return this.http.get<FileInfo[]>(this.config.apiUrl + '/file-explorer' + path); } } diff --git a/client/src/app/shared/components/navbar.component.html b/client/src/app/shared/components/navbar.component.html index 14be398008e56c8ba1ee9dcd4dc86ad31579cd08..6213927093e79526d7ccd9dc4ea7b77d689c2297 100644 --- a/client/src/app/shared/components/navbar.component.html +++ b/client/src/app/shared/components/navbar.component.html @@ -5,7 +5,7 @@ </a> <a *ngIf="instance" routerLink="/instance/{{ instance.name }}" class="navbar-brand"> - <img src="assets/{{ instance.name }}-logo.png" alt="Instance logo" /> + <img src="{{ getLogoHref() }}" alt="Instance logo" /> </a> <!-- Right Navigation --> @@ -64,7 +64,7 @@ </li> <li *ngIf="isAuthenticated" class="divider dropdown-divider"></li> <li *ngFor="let link of links" role="menuitem"> - <a class="dropdown-item" routerLink="link.routerLink"> + <a class="dropdown-item" [routerLink]="link.routerLink"> <span [ngClass]="link.icon" class="fa-fw"></span> {{ link.label }} </a> </li> diff --git a/client/src/app/shared/components/navbar.component.ts b/client/src/app/shared/components/navbar.component.ts index 51379dea9a1831eda3124f85f8f8e6a0e5c0c009..fcbb7457ba7d2b29291c36278c8d2781827c0f8e 100644 --- a/client/src/app/shared/components/navbar.component.ts +++ b/client/src/app/shared/components/navbar.component.ts @@ -24,8 +24,17 @@ export class NavbarComponent { @Input() userProfile: UserProfile = null; @Input() baseHref: string; @Input() authenticationEnabled: boolean; + @Input() apiUrl: string; @Input() instance: Instance; @Output() login: EventEmitter<any> = new EventEmitter(); @Output() logout: EventEmitter<any> = new EventEmitter(); @Output() openEditProfile: EventEmitter<any> = new EventEmitter(); + + getLogoHref() { + if (this.instance.config.design.design_logo) { + return `${this.apiUrl}/download-instance-file/${this.instance.name}/${this.instance.config.design.design_logo}`; + } else { + return 'assets/cesam_anis40.png'; + } + } } diff --git a/client/src/assets/app.config.json b/client/src/assets/app.config.json deleted file mode 100644 index 69f72dd76e9f151588ed89265dd996fff407414e..0000000000000000000000000000000000000000 --- a/client/src/assets/app.config.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "apiUrl": "http://localhost:8080", - "servicesUrl": "http://localhost:5000", - "baseHref": "/", - "authenticationEnabled": false, - "ssoAuthUrl": "http://localhost:8180/auth", - "ssoRealm": "anis", - "ssoClientId": "anis-client", - "adminRole": "anis_admin" -} \ No newline at end of file diff --git a/client/src/environments/environment.prod.ts b/client/src/environments/environment.prod.ts index 5d0833162027e2147e99e72a1a94bb7d3cb62843..5506fedd9829d17697569af187d24a95a4184af4 100644 --- a/client/src/environments/environment.prod.ts +++ b/client/src/environments/environment.prod.ts @@ -1,3 +1,4 @@ export const environment = { - production: true + production: true, + apiUrl: "/server" }; diff --git a/client/src/environments/environment.ts b/client/src/environments/environment.ts index 458476a4df52c64d1a2abee43c6cb91d87abad77..0a202e32320026491d3fdbf7ea375cea6ac5a2cf 100644 --- a/client/src/environments/environment.ts +++ b/client/src/environments/environment.ts @@ -3,7 +3,8 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, + apiUrl: "http://localhost:8080" }; /* diff --git a/client/src/index.html b/client/src/index.html index 82bcb25eaef1535bfdf72649fd25b94976825b6e..341e647c598a2d83fd0db94f9716cf1086d3fef6 100644 --- a/client/src/index.html +++ b/client/src/index.html @@ -2,7 +2,7 @@ <html lang="en" class="h-100"> <head> <meta charset="utf-8"> - <title>Anis - Client</title> + <title id="title">Anis - Client</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" id="favicon" type="image/x-icon" href="favicon.ico"> diff --git a/conf-dev/create-db.sh b/conf-dev/create-db.sh index 464504e5a199237a333cf52dd96c6e720fd1ffbd..e328444a2e7b4e12657874b10532967869eb797c 100644 --- a/conf-dev/create-db.sh +++ b/conf-dev/create-db.sh @@ -59,7 +59,7 @@ curl -d '{"label":"Spectra graph","value":"spectra_graph","display":20,"select_n curl -d '{"label":"Test","dbname":"anis_test","dbtype":"pdo_pgsql","dbhost":"db","dbport":5432,"dblogin":"anis","dbpassword":"anis"}' --header 'Content-Type: application/json' -X POST http://localhost/database # Add default instance -curl -d '{"name":"default","label":"Default instance","client_url":"http://localhost:4200","config":{"design":{"design_color":"#7AC29A"},"search":{"search_by_criteria_allowed":true,"search_multiple_allowed":false,"search_multiple_all_datasets_selected":false},"documentation":{"documentation_allowed":false}}}' --header 'Content-Type: application/json' -X POST http://localhost/instance +curl -d '{"name":"default","label":"Default instance","data_path":"\/DEFAULT","config":{"design":{"design_color":"#7AC29A","design_logo":"logo.png","design_favicon":"favicon.ico"},"home":{"home_component":"WelcomeComponent","home_config":{"home_component_text":"AstroNomical Information System","home_component_logo":"home_component_logo.png"}},"search":{"search_by_criteria_allowed":true,"search_by_criteria_label":"Search","search_multiple_allowed":false,"search_multiple_label":"Search multiple","search_multiple_all_datasets_selected":false},"documentation":{"documentation_allowed":false,"documentation_label":"Documentation"}}}' --header 'Content-Type: application/json' -X POST http://localhost/instance # Add ANIS, SVOM and IRIS surveys curl -d '{"name":"anis_survey","label":"ANIS survey","description":"Survey used for testing","link":"https://anis.lam.fr","manager":"F. Agneray","id_database":1}' --header 'Content-Type: application/json' -X POST http://localhost/survey @@ -72,10 +72,10 @@ curl -d '{"label":"SVOM dataset family","display":20,"opened":true}' --header 'C curl -d '{"label":"IRiS dataset family","display":30,"opened":true}' --header 'Content-Type: application/json' -X POST http://localhost/instance/default/dataset-family # Add datasets -curl -d '{"name":"vipers_dr2_w1","table_ref":"aspic_vipers_dr2_w1","label":"VIPERS-W1 (DR2)","description":"VIPERS W1 dataset","display":10,"data_path":"\/ASPIC\/VIPERS_DR2","config":{"images":[],"cone_search":{"cone_search_enabled":false,"cone_search_opened":true,"cone_search_column_ra":null,"cone_search_column_dec":null,"cone_search_plot_enabled":false,"cone_search_sdss_enabled":true,"cone_search_sdss_display":10,"cone_search_background":[]},"download":{"download_enabled":true,"download_opened":false,"download_csv":true,"download_ascii":true,"download_vo":false,"download_archive":true},"summary":{"summary_enabled":true,"summary_opened":false},"server_link":{"server_link_enabled":false,"server_link_opened":false},"samp":{"samp_enabled":false,"samp_opened":false},"datatable":{"datatable_enabled":true,"datatable_opened":false,"datatable_selectable_rows":false}},"public":true,"survey_name":"anis_survey"}' --header 'Content-Type: application/json' -X POST http://localhost/dataset-family/1/dataset -curl -d '{"name":"sp_cards","table_ref":"sp_cards","label":"SP Metadata","description":"Contains metadata of scientific products (Core Program & General Program)","display":20,"data_path":"","config":{"images":[],"cone_search":{"cone_search_enabled":false,"cone_search_opened":true,"cone_search_column_ra":null,"cone_search_column_dec":null,"cone_search_plot_enabled":false,"cone_search_sdss_enabled":true,"cone_search_sdss_display":10,"cone_search_background":[]},"download":{"download_enabled":true,"download_opened":false,"download_csv":true,"download_ascii":true,"download_vo":false,"download_archive":true},"summary":{"summary_enabled":true,"summary_opened":false},"server_link":{"server_link_enabled":false,"server_link_opened":false},"samp":{"samp_enabled":false,"samp_opened":false},"datatable":{"datatable_enabled":true,"datatable_opened":false,"datatable_selectable_rows":false}},"public":true,"survey_name":"svom"}' --header 'Content-Type: application/json' -X POST http://localhost/dataset-family/2/dataset -curl -d '{"name":"observations","table_ref":"v_observation","label":"IRiS obs","description":"IRiS observations","display":10,"data_path":"\/IRIS\/observations","config":{"images":[],"cone_search":{"cone_search_enabled":false,"cone_search_opened":true,"cone_search_column_ra":null,"cone_search_column_dec":null,"cone_search_plot_enabled":false,"cone_search_sdss_enabled":true,"cone_search_sdss_display":10,"cone_search_background":[]},"download":{"download_enabled":true,"download_opened":false,"download_csv":true,"download_ascii":true,"download_vo":false,"download_archive":true},"summary":{"summary_enabled":true,"summary_opened":false},"server_link":{"server_link_enabled":false,"server_link_opened":false},"samp":{"samp_enabled":false,"samp_opened":false},"datatable":{"datatable_enabled":true,"datatable_opened":false,"datatable_selectable_rows":false}},"public":true,"survey_name":"iris"}' --header 'Content-Type: application/json' -X POST http://localhost/dataset-family/3/dataset -curl -d '{"name":"vvds_f02_udeep","table_ref":"aspic_vvds_f02_udeep","label":"VVDS2h Ultra Deep","description":"VVDS2h Ultra Deep","display":20,"data_path":"","config":{"images":[],"cone_search":{"cone_search_enabled":false,"cone_search_opened":true,"cone_search_column_ra":null,"cone_search_column_dec":null,"cone_search_plot_enabled":false,"cone_search_sdss_enabled":true,"cone_search_sdss_display":10,"cone_search_background":[]},"download":{"download_enabled":true,"download_opened":false,"download_csv":true,"download_ascii":true,"download_vo":false,"download_archive":true},"summary":{"summary_enabled":true,"summary_opened":false},"server_link":{"server_link_enabled":false,"server_link_opened":false},"samp":{"samp_enabled":false,"samp_opened":false},"datatable":{"datatable_enabled":true,"datatable_opened":false,"datatable_selectable_rows":false}},"public":true,"survey_name":"anis_survey"}' --header 'Content-Type: application/json' -X POST http://localhost/dataset-family/1/dataset +curl -d '{"name":"vipers_dr2_w1","table_ref":"aspic_vipers_dr2_w1","label":"VIPERS-W1 (DR2)","description":"VIPERS W1 dataset","display":10,"data_path":"\/ASPIC\/VIPERS_DR2","config":{"images":[],"survey":{"survey_enabled":true,"survey_label":"More about this survey"},"cone_search":{"cone_search_enabled":false,"cone_search_opened":true,"cone_search_column_ra":null,"cone_search_column_dec":null,"cone_search_plot_enabled":false,"cone_search_sdss_enabled":true,"cone_search_sdss_display":10,"cone_search_background":[]},"download":{"download_enabled":true,"download_opened":false,"download_csv":true,"download_ascii":true,"download_vo":false,"download_archive":true},"summary":{"summary_enabled":true,"summary_opened":false},"server_link":{"server_link_enabled":false,"server_link_opened":false},"samp":{"samp_enabled":false,"samp_opened":false},"datatable":{"datatable_enabled":true,"datatable_opened":false,"datatable_selectable_rows":false}},"public":true,"survey_name":"anis_survey"}' --header 'Content-Type: application/json' -X POST http://localhost/dataset-family/1/dataset +curl -d '{"name":"sp_cards","table_ref":"sp_cards","label":"SP Metadata","description":"Contains metadata of scientific products (Core Program & General Program)","display":20,"data_path":"","config":{"images":[],"survey":{"survey_enabled":true,"survey_label":"More about this survey"},"cone_search":{"cone_search_enabled":false,"cone_search_opened":true,"cone_search_column_ra":null,"cone_search_column_dec":null,"cone_search_plot_enabled":false,"cone_search_sdss_enabled":true,"cone_search_sdss_display":10,"cone_search_background":[]},"download":{"download_enabled":true,"download_opened":false,"download_csv":true,"download_ascii":true,"download_vo":false,"download_archive":true},"summary":{"summary_enabled":true,"summary_opened":false},"server_link":{"server_link_enabled":false,"server_link_opened":false},"samp":{"samp_enabled":false,"samp_opened":false},"datatable":{"datatable_enabled":true,"datatable_opened":false,"datatable_selectable_rows":false}},"public":true,"survey_name":"svom"}' --header 'Content-Type: application/json' -X POST http://localhost/dataset-family/2/dataset +curl -d '{"name":"observations","table_ref":"v_observation","label":"IRiS obs","description":"IRiS observations","display":10,"data_path":"\/IRIS\/observations","config":{"images":[],"survey":{"survey_enabled":true,"survey_label":"More about this survey"},"cone_search":{"cone_search_enabled":false,"cone_search_opened":true,"cone_search_column_ra":null,"cone_search_column_dec":null,"cone_search_plot_enabled":false,"cone_search_sdss_enabled":true,"cone_search_sdss_display":10,"cone_search_background":[]},"download":{"download_enabled":true,"download_opened":false,"download_csv":true,"download_ascii":true,"download_vo":false,"download_archive":true},"summary":{"summary_enabled":true,"summary_opened":false},"server_link":{"server_link_enabled":false,"server_link_opened":false},"samp":{"samp_enabled":false,"samp_opened":false},"datatable":{"datatable_enabled":true,"datatable_opened":false,"datatable_selectable_rows":false}},"public":true,"survey_name":"iris"}' --header 'Content-Type: application/json' -X POST http://localhost/dataset-family/3/dataset +curl -d '{"name":"vvds_f02_udeep","table_ref":"aspic_vvds_f02_udeep","label":"VVDS2h Ultra Deep","description":"VVDS2h Ultra Deep","display":20,"data_path":"","config":{"images":[],"survey":{"survey_enabled":true,"survey_label":"More about this survey"},"cone_search":{"cone_search_enabled":false,"cone_search_opened":true,"cone_search_column_ra":null,"cone_search_column_dec":null,"cone_search_plot_enabled":false,"cone_search_sdss_enabled":true,"cone_search_sdss_display":10,"cone_search_background":[]},"download":{"download_enabled":true,"download_opened":false,"download_csv":true,"download_ascii":true,"download_vo":false,"download_archive":true},"summary":{"summary_enabled":true,"summary_opened":false},"server_link":{"server_link_enabled":false,"server_link_opened":false},"samp":{"samp_enabled":false,"samp_opened":false},"datatable":{"datatable_enabled":true,"datatable_opened":false,"datatable_selectable_rows":false}},"public":true,"survey_name":"anis_survey"}' --header 'Content-Type: application/json' -X POST http://localhost/dataset-family/1/dataset # Add vipers_dr2_w1 attributes curl -d '{"label":"Default","display":10,"opened":true}' --header 'Content-Type: application/json' -X POST http://localhost/dataset/vipers_dr2_w1/criteria-family diff --git a/conf-dev/public_key b/conf-dev/public_key index ebb23d4c7bfdc0bec2e326b8df170f22e3412c5d..3c3ba0e8be160eac8c137f66f4bfb969137c8c04 100644 --- a/conf-dev/public_key +++ b/conf-dev/public_key @@ -1,3 +1,3 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi5V9FJj0dI/2TRDWoZzSKDa8l5N81sCQUunZaEuyBx3EW5cDioL2ktdc3LCB5u+rVQzYD6c3b24eLbgwgYx8AQ03GXW63TkuUy7oEA1XtBicNX/IO51ITUCeUJfJpUI+iGDEK4EmeVBiaVUTrQ8L/SMTQUcRPESNwaRmFov9kkNDiPaNQpAzbSillJLdQG9oOIKDpqjXW+ZOBct1J//+8+f0vHibbDt2HacFrq2z2ahv10ESnxqtnzjMMcn0e/IDIiolsQpcCpEwaBBqJ6axUiKReJBXU/IsFn/GtemLwPo/MpthjIi1rfqPvtin25ecR9VAWRW0bLdqztnMsfJ3oQIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1fucBQK34tl8Gx/fefATnpWqW5PVlwMYcJAqgWvmtwNm9ZW/S5HNZZfjW1S9BOJLfudCM83WHrAwGixgHKI310YXg+6BI9qn2Gnge1GC3JtKZx6UdcxZFAYmlhY0QGL5QxGR58DkEj6l/FDrwAHyVkC5sLqDMiYsqO7CA1uJLtF8yUrCyHvI4TR+kk5ZSM94/osg6eGxGSYA89u+qhG5tz5YCFgiRUNxuAEucsz8XiEfNVAz5kdYgsR4+LtmqECfczpwcJrAu7yDxs3rQPjBqFdGqEehALHX9vq71VEYe5Id2P7ddik3byHa0a21Q0RuVhMYwGrLiMLJCXqxE1YMuwIDAQAB -----END PUBLIC KEY----- diff --git a/docker-compose.yml b/docker-compose.yml index bc9bd7b06cecef4ee1d3fbb01266056a56649ce1..ee412b76725a54d72cfad75ec8636d000a5842f9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,11 @@ services: LOGGER_NAME: "anis-metamodel" LOGGER_PATH: "php://stderr" LOGGER_LEVEL: "debug" + SERVICES_URL: "http://localhost:5000" + BASE_HREF: "/" + SSO_AUTH_URL: "http://localhost:8180/auth" + SSO_REALM: "anis" + SSO_CLIENT_ID: "anis-client" TOKEN_ENABLED: 0 TOKEN_PUBLIC_KEY_FILE: /mnt/public_key TOKEN_ADMIN_ROLE: anis_admin diff --git a/server/.gitlab-ci.yml b/server/.gitlab-ci.yml index 11df89cf2a14fa312773d1a46deb0614a7433e07..5cdac1406a6deb72933c05f83c0fc7d48dcf8af6 100644 --- a/server/.gitlab-ci.yml +++ b/server/.gitlab-ci.yml @@ -3,6 +3,7 @@ stages: - test - sonar - dockerize + - deploy variables: VERSION: "3.7" @@ -70,3 +71,17 @@ dockerize: - docker pull $CI_REGISTRY/anis/anis-next/server:latest || true - docker build --cache-from $CI_REGISTRY/anis/anis-next/server:latest -t $CI_REGISTRY/anis/anis-next/server:latest . - docker push $CI_REGISTRY/anis/anis-next/server:latest + +deploy: + image: alpine + stage: deploy + variables: + GIT_STRATEGY: none + cache: {} + dependencies: [] + script: + - apk add --update curl + - curl -XPOST $DEV_WEBHOOK_SERVER + only: + refs: + - develop \ No newline at end of file diff --git a/server/app/dependencies.php b/server/app/dependencies.php index 1406024f2a5e62027beefb897ee7d817b7c58436..2ba1d1681c1f8c71cb792a2c0ff90614ef782108 100644 --- a/server/app/dependencies.php +++ b/server/app/dependencies.php @@ -56,6 +56,10 @@ $container->set('App\Action\RootAction', function () { return new App\Action\RootAction(); }); +$container->set('App\Action\ClientSettingsAction', function (ContainerInterface $c) { + return new App\Action\ClientSettingsAction($c->get(SETTINGS)); +}); + $container->set('App\Action\SelectListAction', function (ContainerInterface $c) { return new App\Action\SelectListAction($c->get('em')); }); @@ -218,6 +222,10 @@ $container->set('App\Action\DatasetFileExplorerAction', function (ContainerInter return new App\Action\DatasetFileExplorerAction($c->get('em'), $c->get('settings')['data_path'], $c->get(SETTINGS)['token']); }); +$container->set('App\Action\DownloadInstanceFileAction', function (ContainerInterface $c) { + return new App\Action\DownloadInstanceFileAction($c->get('em'), $c->get('settings')['data_path']); +}); + $container->set('App\Action\DownloadFileAction', function (ContainerInterface $c) { return new App\Action\DownloadFileAction($c->get('em'), $c->get('settings')['data_path'], $c->get(SETTINGS)['token']); }); diff --git a/server/app/routes.php b/server/app/routes.php index 76ef14eeea4541d2b41b0776c498bac4e4054dc6..42b9479b79dc9da018607214cc06690a32bfe7dc 100644 --- a/server/app/routes.php +++ b/server/app/routes.php @@ -13,6 +13,7 @@ declare(strict_types=1); use Slim\Routing\RouteCollectorProxy; $app->get('/', App\Action\RootAction::class); +$app->get('/client-settings', App\Action\ClientSettingsAction::class); $app->group('', function (RouteCollectorProxy $group) { $group->map([OPTIONS, GET, POST], '/select', App\Action\SelectListAction::class); @@ -69,4 +70,5 @@ $app->group('', function (RouteCollectorProxy $group) { $app->get('/search/{dname}', App\Action\SearchAction::class); $app->get('/archive/{dname}', App\Action\ArchiveAction::class); $app->get('/dataset-file-explorer/{dname}[{fpath:.*}]', App\Action\DatasetFileExplorerAction::class); +$app->get('/download-instance-file/{iname}/[{fpath:.*}]', App\Action\DownloadInstanceFileAction::class); $app->get('/download-file/{dname}/[{fpath:.*}]', App\Action\DownloadFileAction::class); diff --git a/server/app/settings.php b/server/app/settings.php index a37c755705f852b00796fabacbd0b66aaf054099..f018b5675ac88559c6948d89061be79ee73f8acb 100644 --- a/server/app/settings.php +++ b/server/app/settings.php @@ -31,8 +31,14 @@ return [ 'path' => getenv('LOGGER_PATH'), 'level' => getenv('LOGGER_LEVEL') ], + 'services_url' => getenv('SERVICES_URL'), + 'base_href' => getenv('BASE_HREF'), + 'sso' => [ + 'auth_url' => getenv('SSO_AUTH_URL'), + 'realm' => getenv('SSO_REALM'), + 'client_id' => getenv('SSO_CLIENT_ID') + ], 'token' => [ - //'issuer' => getenv('TOKEN_ISSUER'), 'enabled' => getenv('TOKEN_ENABLED'), 'public_key_file' => getenv('TOKEN_PUBLIC_KEY_FILE'), 'admin_role' => getenv('TOKEN_ADMIN_ROLE') diff --git a/server/doctrine-proxy/__CG__AppEntityInstance.php b/server/doctrine-proxy/__CG__AppEntityInstance.php index 31548dc0c004bfdcb349fc852549eb52f57781be..b971ab686cd1cbd46df3c1149c130ab3998ad7bc 100644 --- a/server/doctrine-proxy/__CG__AppEntityInstance.php +++ b/server/doctrine-proxy/__CG__AppEntityInstance.php @@ -66,10 +66,10 @@ class Instance extends \App\Entity\Instance implements \Doctrine\ORM\Proxy\Proxy public function __sleep() { if ($this->__isInitialized__) { - return ['__isInitialized__', 'name', 'label', 'clientUrl', 'config', 'datasetFamilies']; + return ['__isInitialized__', 'name', 'label', 'dataPath', 'config', 'datasetFamilies']; } - return ['__isInitialized__', 'name', 'label', 'clientUrl', 'config', 'datasetFamilies']; + return ['__isInitialized__', 'name', 'label', 'dataPath', 'config', 'datasetFamilies']; } /** @@ -216,23 +216,23 @@ class Instance extends \App\Entity\Instance implements \Doctrine\ORM\Proxy\Proxy /** * {@inheritDoc} */ - public function getClientUrl() + public function getDataPath() { - $this->__initializer__ && $this->__initializer__->__invoke($this, 'getClientUrl', []); + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getDataPath', []); - return parent::getClientUrl(); + return parent::getDataPath(); } /** * {@inheritDoc} */ - public function setClientUrl($clientUrl) + public function setDataPath($dataPath) { - $this->__initializer__ && $this->__initializer__->__invoke($this, 'setClientUrl', [$clientUrl]); + $this->__initializer__ && $this->__initializer__->__invoke($this, 'setDataPath', [$dataPath]); - return parent::setClientUrl($clientUrl); + return parent::setDataPath($dataPath); } /** diff --git a/server/src/Action/ClientSettingsAction.php b/server/src/Action/ClientSettingsAction.php new file mode 100644 index 0000000000000000000000000000000000000000..1018ac39d0a8853cd63592936b6a385b00218ac8 --- /dev/null +++ b/server/src/Action/ClientSettingsAction.php @@ -0,0 +1,61 @@ +<?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\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Client settings action + * + * @author François Agneray <francois.agneray@lam.fr> + * @package App\Action + */ +final class ClientSettingsAction +{ + /** + * The ANIS settings array + * + * @var array + */ + private $settings; + + public function __construct($settings) + { + $this->settings = $settings; + } + + /** + * This action indicates that the service is responding + * + * @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(Request $request, Response $response, array $args): Response + { + $payload = json_encode(array( + 'servicesUrl' => $this->settings['services_url'], + 'baseHref' => $this->settings['base_href'], + 'authenticationEnabled' => boolval($this->settings['token']['enabled']), + 'ssoAuthUrl' => $this->settings['sso']['auth_url'], + 'ssoRealm' => $this->settings['sso']['realm'], + 'ssoClientId' => $this->settings['sso']['client_id'], + 'adminRole' => $this->settings['token']['admin_role'] + )); + $response->getBody()->write($payload); + return $response; + } +} diff --git a/server/src/Action/DatasetFileExplorerAction.php b/server/src/Action/DatasetFileExplorerAction.php index 91a925ebd2d3d175766973730f9fb21e39b04285..33a17d3c616638e9958fe529dfd7275c7e545feb 100644 --- a/server/src/Action/DatasetFileExplorerAction.php +++ b/server/src/Action/DatasetFileExplorerAction.php @@ -84,8 +84,9 @@ final class DatasetFileExplorerAction extends AbstractAction $this->verifyDatasetAuthorization($request, $dataset->getName(), $this->settings['admin_role']); } - $path = $this->dataPath . $dataset->getDataPath(); - + // Dataset data_path + $path = $this->dataPath . $dataset->getFullDataPath(); + if (array_key_exists('fpath', $args)) { $path .= DIRECTORY_SEPARATOR . $args['fpath']; } diff --git a/server/src/Action/DownloadFileAction.php b/server/src/Action/DownloadFileAction.php index 21736352b6905e071acfe11efcc1897da4da51e9..4d3b3d5739a0b472f5f58cd581dabff516073ac9 100644 --- a/server/src/Action/DownloadFileAction.php +++ b/server/src/Action/DownloadFileAction.php @@ -85,7 +85,7 @@ final class DownloadFileAction extends AbstractAction } // Search the file - $filePath = $this->dataPath . $dataset->getDataPath() . DIRECTORY_SEPARATOR . $args['fpath']; + $filePath = $this->dataPath . $dataset->getFullDataPath() . DIRECTORY_SEPARATOR . $args['fpath']; // If the file not found 404 if (!file_exists($filePath)) { diff --git a/server/src/Action/DownloadInstanceFileAction.php b/server/src/Action/DownloadInstanceFileAction.php new file mode 100644 index 0000000000000000000000000000000000000000..b9eed921ce2185d22804943fa4031843e4cecb55 --- /dev/null +++ b/server/src/Action/DownloadInstanceFileAction.php @@ -0,0 +1,93 @@ +<?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 as Request; +use Psr\Http\Message\ResponseInterface as Response; +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 DownloadInstanceFileAction extends AbstractAction +{ + /** + * Contains anis-server data path + * + * @var string + */ + private $dataPath; + + /** + * 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 + */ + public function __construct(EntityManagerInterface $em, string $dataPath) + { + parent::__construct($em); + $this->dataPath = $dataPath; + } + + /** + * `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(Request $request, Response $response, array $args): Response + { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + } + + // Search the correct instance with primary key + $instanceName = $args['iname']; + $instance = $this->em->find('App\Entity\Instance', $instanceName); + + // If dataset is not found 404 + if (is_null($instance)) { + throw new HttpNotFoundException( + $request, + 'Instance with name ' . $instanceName . ' is not found' + ); + } + + // Search the file + $filePath = $this->dataPath . $instance->getDataPath() . DIRECTORY_SEPARATOR . $args['fpath']; + + // If the file not found 404 + if (!file_exists($filePath)) { + throw new HttpNotFoundException( + $request, + 'File with path ' . $args['fpath'] . ' is not found for the instance ' . $instanceName + ); + } + + // 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/InstanceAction.php b/server/src/Action/InstanceAction.php index d6596c946d2807b5afb508d591877f609244f696..82442eb8881917e3e9b2c9292758a7c513ec383c 100644 --- a/server/src/Action/InstanceAction.php +++ b/server/src/Action/InstanceAction.php @@ -60,7 +60,7 @@ final class InstanceAction extends AbstractAction $parsedBody = $request->getParsedBody(); // If mandatories empty fields 400 - foreach (array('label') as $a) { + foreach (array('label', 'data_path') as $a) { if ($this->isEmptyField($a, $parsedBody)) { throw new HttpBadRequestException( $request, @@ -93,12 +93,8 @@ final class InstanceAction extends AbstractAction private function editInstance(Instance $instance, array $parsedBody): void { $instance->setLabel($parsedBody['label']); - if (!$this->isEmptyField('client_url', $parsedBody)) { - $instance->setClientUrl($parsedBody['client_url']); - } else { - $instance->setClientUrl(null); - } $instance->setConfig($parsedBody['config']); + $instance->setDataPath($parsedBody['data_path']); $this->em->flush(); } } diff --git a/server/src/Action/InstanceListAction.php b/server/src/Action/InstanceListAction.php index ba42f865b198abb1a3b8b6691a4e8d44da2e2de0..9e226441a04659f0e18a5ae2b3ce00c08ef3c6a6 100644 --- a/server/src/Action/InstanceListAction.php +++ b/server/src/Action/InstanceListAction.php @@ -48,7 +48,7 @@ final class InstanceListAction extends AbstractAction $parsedBody = $request->getParsedBody(); // To work this action needs user information to update - foreach (array('name', 'label') as $a) { + foreach (array('name', 'label', 'data_path') as $a) { if ($this->isEmptyField($a, $parsedBody)) { throw new HttpBadRequestException( $request, @@ -76,10 +76,8 @@ final class InstanceListAction extends AbstractAction private function postInstance(array $parsedBody): Instance { $instance = new Instance($parsedBody['name'], $parsedBody['label']); - if (!$this->isEmptyField('client_url', $parsedBody)) { - $instance->setClientUrl($parsedBody['client_url']); - } $instance->setConfig($parsedBody['config']); + $instance->setDataPath($parsedBody['data_path']); $this->em->persist($instance); $this->em->flush(); diff --git a/server/src/Entity/Dataset.php b/server/src/Entity/Dataset.php index 45fec28a6bde0e364183fd5093294bde3c59793c..676fe3599d07bf993cda62a9482dd7a658becba0 100644 --- a/server/src/Entity/Dataset.php +++ b/server/src/Entity/Dataset.php @@ -209,6 +209,11 @@ class Dataset implements \JsonSerializable return $this->attributes; } + public function getFullDataPath() + { + return $this->getDatasetFamily()->getInstance()->getDataPath() . $this->getDataPath(); + } + public function jsonSerialize() { return [ @@ -221,7 +226,8 @@ class Dataset implements \JsonSerializable 'config' => $this->getConfig(), 'public' => $this->getPublic(), 'survey_name' => $this->getSurvey()->getName(), - 'id_dataset_family' => $this->getDatasetFamily()->getId() + 'id_dataset_family' => $this->getDatasetFamily()->getId(), + 'full_data_path' => $this->getFullDataPath() ]; } } diff --git a/server/src/Entity/Instance.php b/server/src/Entity/Instance.php index 1e6ac070c1b050f681991a8d7268caba8184b32a..d95670e1f5ddbf8e1bb5eb7daf2eb1a584c7e566 100644 --- a/server/src/Entity/Instance.php +++ b/server/src/Entity/Instance.php @@ -41,9 +41,9 @@ class Instance implements \JsonSerializable /** * @var string * - * @Column(type="string", name="client_url", nullable=true) + * @Column(type="string", name="data_path", nullable=true) */ - protected $clientUrl; + protected $dataPath; /** * @var string @@ -81,14 +81,14 @@ class Instance implements \JsonSerializable $this->label = $label; } - public function getClientUrl() + public function getDataPath() { - return $this->clientUrl; + return $this->dataPath; } - public function setClientUrl($clientUrl) + public function setDataPath($dataPath) { - $this->clientUrl = $clientUrl; + $this->dataPath = $dataPath; } public function getConfig() @@ -120,7 +120,7 @@ class Instance implements \JsonSerializable return [ 'name' => $this->getName(), 'label' => $this->getLabel(), - 'client_url' => $this->getClientUrl(), + 'data_path' => $this->getDataPath(), 'config' => $this->getConfig(), 'nb_dataset_families' => count($this->getDatasetFamilies()), 'nb_datasets' => $this->getNbDatasets() diff --git a/server/src/Middleware/AuthorizationMiddleware.php b/server/src/Middleware/AuthorizationMiddleware.php index b17c9aaae73870f3e428a0648a8ecfefeb8df4db..82b5b4e040383a817331333364af1065016b4012 100644 --- a/server/src/Middleware/AuthorizationMiddleware.php +++ b/server/src/Middleware/AuthorizationMiddleware.php @@ -57,15 +57,20 @@ final class AuthorizationMiddleware implements MiddlewareInterface { if ( $request->getMethod() === OPTIONS - || !$request->hasHeader('Authorization') + || (!$request->hasHeader('Authorization') && !array_key_exists('token', $request->getQueryParams())) || !boolval($this->settings['enabled']) ) { return $handler->handle($request); } // Get token string from Authorizarion header - $bearer = $request->getHeader('Authorization'); - $data = explode(' ', $bearer[0]); + if ($request->hasHeader('Authorization')) { + $bearer = $request->getHeader('Authorization')[0]; + } else { + $bearer = 'Bearer ' . $request->getQueryParams()['token']; + } + + $data = explode(' ', $bearer); if ($data[0] !== 'Bearer') { return $this->getUnauthorizedResponse( 'HTTP 401: Authorization must contain a string with the following format -> Bearer JWT' @@ -89,12 +94,12 @@ final class AuthorizationMiddleware implements MiddlewareInterface * * @return Response */ - private function getUnauthorizedResponse(string $message): Response + private function getUnauthorizedResponse(string $message): NyholmResponse { $resonse = new NyholmResponse(); $resonse->getBody()->write(json_encode(array( 'message' => $message ))); - return $resonse->withStatus(401); + return $resonse->withStatus(401)->withHeader('Access-Control-Allow-Origin', '*'); } } diff --git a/server/src/Middleware/RouteGuardMiddleware.php b/server/src/Middleware/RouteGuardMiddleware.php index 89bcf705156f828afd46505f4999603424b70b45..a9ba0e56e36a9f2578e18d1b145776f245f09642 100644 --- a/server/src/Middleware/RouteGuardMiddleware.php +++ b/server/src/Middleware/RouteGuardMiddleware.php @@ -102,6 +102,6 @@ final class RouteGuardMiddleware implements MiddlewareInterface $resonse->getBody()->write(json_encode(array( 'message' => $message ))); - return $resonse->withStatus($code); + return $resonse->withStatus($code)->withHeader('Access-Control-Allow-Origin', '*'); } } diff --git a/server/tests/Action/DatasetListActionTest.php b/server/tests/Action/DatasetListActionTest.php index 3b34d4e20c67f7f3ad8e7285487c5f1090e6f1ff..51a5a343ceb1d6b63fc53ac3346fd35117c183df 100644 --- a/server/tests/Action/DatasetListActionTest.php +++ b/server/tests/Action/DatasetListActionTest.php @@ -21,6 +21,7 @@ use Doctrine\ORM\EntityManager; use Doctrine\Persistence\ObjectRepository; use App\Entity\Survey; use App\Entity\DatasetFamily; +use App\Entity\Instance; final class DatasetListActionTest extends TestCase { @@ -91,7 +92,9 @@ final class DatasetListActionTest extends TestCase public function testAddANewDataset(): void { + $instance = $this->createMock(Instance::class); $datasetFamily = $this->createMock(DatasetFamily::class); + $datasetFamily->method('getInstance')->willReturn($instance); $survey = $this->createMock(Survey::class); $this->entityManager->method('find')->willReturnOnConsecutiveCalls($datasetFamily, $survey); diff --git a/server/tests/Action/InstanceActionTest.php b/server/tests/Action/InstanceActionTest.php index a6c2a0ce106cd1b704a0e21bc1ff02eee76d43cb..5b717a3a2dd358c5e8b38e160f66b430c55a6870 100644 --- a/server/tests/Action/InstanceActionTest.php +++ b/server/tests/Action/InstanceActionTest.php @@ -78,7 +78,7 @@ final class InstanceActionTest extends TestCase $fields = array( 'label' => 'AspiC', - 'client_url' => 'http://aspic.lam.fr', + 'data_path' => '/DEFAULT', 'config' => '{}' ); diff --git a/server/tests/Action/InstanceListActionTest.php b/server/tests/Action/InstanceListActionTest.php index 84ec13d8eaa930c6fcab76cc2679b26966335ffe..81c86466500f8515caccb29bbd7de4cb70e3a11e 100644 --- a/server/tests/Action/InstanceListActionTest.php +++ b/server/tests/Action/InstanceListActionTest.php @@ -63,7 +63,7 @@ final class InstanceListActionTest extends TestCase $fields = array( 'name' => 'aspic', 'label' => 'Aspic', - 'client_url' => 'http://cesam.lam.fr/aspic', + 'data_path' => '/DEFAULT', 'config' => '{}' ); diff --git a/services/.gitlab-ci.yml b/services/.gitlab-ci.yml index 38f978456fbe7706e1ad8a1e9a650756a6bc4f05..4bfe6473df99eef1ae1f2c6c0752f7ab3184c581 100644 --- a/services/.gitlab-ci.yml +++ b/services/.gitlab-ci.yml @@ -1,5 +1,6 @@ stages: - dockerize + - deploy variables: VERSION: "3.7" @@ -15,3 +16,17 @@ dockerize: - docker pull $CI_REGISTRY/anis/anis-next/services:latest || true - docker build --cache-from $CI_REGISTRY/anis/anis-next/services:latest -t $CI_REGISTRY/anis/anis-next/services:latest . - docker push $CI_REGISTRY/anis/anis-next/services:latest + +deploy: + image: alpine + stage: deploy + variables: + GIT_STRATEGY: none + cache: {} + dependencies: [] + script: + - apk add --update curl + - curl -XPOST $DEV_WEBHOOK_SERVICES + only: + refs: + - develop \ No newline at end of file diff --git a/services/src/app.py b/services/src/app.py index 8b31a4c3ad5ce53f17964296257ba9cd226d5736..742b8598afb46e0031c58ecff27949390eec2873 100644 --- a/services/src/app.py +++ b/services/src/app.py @@ -163,7 +163,7 @@ def spectra_to_csv(dname): try: file_path = utils.get_file_path(dname, filename) - csv = spectra.spectra_to_csv(file_path) + csv = spectra.spectra_to_csv(file_path, filename) except utils.DatasetNotFound as e: return {"message": str(e)}, 404 except utils.FileForbidden as e: diff --git a/services/src/spectra.py b/services/src/spectra.py index 1fefa25b5056af9dcfa46f20bdcda4741f99e98a..fab05092be39c3f612fc107b96b3efc30ca9ae51 100644 --- a/services/src/spectra.py +++ b/services/src/spectra.py @@ -9,8 +9,8 @@ from spectra_strategies import GamaDR2AATStrategy from spectra_strategies import GamaDR2LTStrategy from spectra_strategies import ZCosmosBrightDr3Strategy -def spectra_to_csv(filename): - hdulist = fits.open(filename) +def spectra_to_csv(file_path, filename): + hdulist = fits.open(file_path) if(filename.count('6dF') > 0): solver = StrategySolver(SixdFStrategy()) diff --git a/services/src/utils.py b/services/src/utils.py index ae8d5ce85665147c0fadb9f2a28dafc6417df3af..ff51fcdc92a73d8cd751f4f54d4b731972035c3e 100644 --- a/services/src/utils.py +++ b/services/src/utils.py @@ -28,7 +28,7 @@ def get_dataset(dname): def get_file_path(dname, filename): data_path = os.environ["DATA_PATH"] dataset = get_dataset(dname) - dataset_data_path = dataset["data_path"] + dataset_data_path = dataset["full_data_path"] file_path = data_path + dataset_data_path + '/' + filename if ('..' in filename):