Commit ca9c1a21 authored by François Agneray's avatar François Agneray
Browse files

Merge branch '82-add-samp-access' into 'develop'

Resolve "Add SAMP access"

Closes #82

See merge request !173
parents 6cdfc00c 90bfb5fb
Pipeline #5417 passed with stages
in 13 minutes and 16 seconds
......@@ -33,7 +33,8 @@
"src/styles.css"
],
"scripts": [
"node_modules/@fortawesome/fontawesome-free/js/all.js"
"node_modules/@fortawesome/fontawesome-free/js/all.js",
"src/assets/samp.js"
]
},
"configurations": {
......
......@@ -64,4 +64,4 @@
"tslint": "~6.1.0",
"typescript": "~4.1.5"
}
}
\ No newline at end of file
}
......@@ -26,19 +26,35 @@ export interface Dataset {
config: {
cone_search: {
enabled: boolean;
opened: boolean;
column_ra: number;
column_dec: number;
plot_enabled: boolean;
};
result_page_modules: {
results_server_link: boolean;
opened_datatable: boolean;
selectable_row: boolean;
}
results_format: {
},
download: {
enabled: boolean;
opened: boolean;
csv: boolean;
ascii: boolean;
vo: boolean;
};
archive: boolean;
},
summary: {
enabled: boolean;
opened: boolean;
},
server_link: {
enabled: boolean;
opened: boolean;
},
samp: {
enabled: boolean;
opened: boolean;
},
datatable: {
enabled: boolean;
opened: boolean;
selectable_row: boolean;
}
};
}
/**
* 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 { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { SampEffects } from './store/samp.effects';
import { SampService } from './store/samp.service';
import { reducer } from './store/samp.reducer';
@NgModule({
imports: [
StoreModule.forFeature('samp', reducer),
EffectsModule.forFeature([ SampEffects ])
],
providers: [
SampService
]
})
/**
* @class
* @classdesc Samp module.
*/
export class SampModule { }
/**
* 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 { Action } from '@ngrx/store';
export const REGISTER = '[Samp] Register';
export const REGISTER_SUCCESS = '[Samp] Register Success';
export const REGISTER_FAIL = '[Samp] Register Fail';
export const UNREGISTER = '[Samp] unregister';
export const BROADCAST_VOTABLE = '[Samp] Broadcast Votable';
/**
* @class
* @classdesc Register action.
* @readonly
*/
export class RegisterAction implements Action {
readonly type = REGISTER;
constructor(public payload: {} = null) { }
}
export class RegisterSuccessAction implements Action {
readonly type = REGISTER_SUCCESS;
constructor(public payload: {} = null) { }
}
export class RegisterFailAction implements Action {
readonly type = REGISTER_FAIL;
constructor(public payload: {} = null) { }
}
/**
* @class
* @classdesc Unregister action.
* @readonly
*/
export class UnregisterAction implements Action {
readonly type = UNREGISTER;
constructor(public payload: {} = null) { }
}
/**
* @class
* @classdesc Broadcast votable action.
* @readonly
*/
export class BroadcastVotableAction implements Action {
readonly type = BROADCAST_VOTABLE;
constructor(public payload: string) { }
}
export type Actions
= RegisterAction
| RegisterSuccessAction
| RegisterFailAction
| UnregisterAction
| BroadcastVotableAction;
/**
* 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 } from '@ngrx/effects';
import { of } from 'rxjs';
import { tap, map, switchMap, catchError } from 'rxjs/operators';
import { ToastrService } from 'ngx-toastr';
import { SampService } from './samp.service';
import * as sampActions from './samp.action';
@Injectable()
/**
* @class
* @classdesc Search effects.
*/
export class SampEffects {
constructor(
private actions$: Actions,
private sampService: SampService,
private toastr: ToastrService
) { }
register$ = createEffect(() =>
this.actions$.pipe(
ofType(sampActions.REGISTER),
switchMap(_ => this.sampService.register().pipe(
map(_ => new sampActions.RegisterSuccessAction()),
catchError(_ => of(new sampActions.RegisterFailAction()))
))
)
);
registerSuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(sampActions.REGISTER_SUCCESS),
tap(_ => this.toastr.success('You are now connected to a SAMP-hub', 'SAMP-hub register success'))
),
{ dispatch: false }
);
registerFail$ = createEffect(() =>
this.actions$.pipe(
ofType(sampActions.REGISTER_FAIL),
tap(_ => this.toastr.error('Connection to a SAMP-hub has failed', 'SAMP-hub register fail'))
),
{ dispatch: false }
);
unregister$ = createEffect(() =>
this.actions$.pipe(
ofType(sampActions.UNREGISTER),
tap(_ => {
this.sampService.unregister();
})
),
{ dispatch: false }
);
broadcastVotable$ = createEffect(() =>
this.actions$.pipe(
ofType(sampActions.BROADCAST_VOTABLE),
tap((action: sampActions.BroadcastVotableAction) => {
this.sampService.broadcast('table.load.votable', action.payload)
})
),
{ dispatch: false }
);
}
/**
* 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 * as actions from './samp.action';
/**
* Interface for samp state.
*
* @interface State
*/
export interface State {
registered: boolean;
}
export const initialState: State = {
registered: false
};
/**
* Reduces state.
*
* @param {State} state - The state.
* @param {actions} action - The action.
*
* @return State
*/
export function reducer(state: State = initialState, action: actions.Actions): State {
switch (action.type) {
case actions.REGISTER_SUCCESS:
return {
...state,
registered: true
};
case actions.UNREGISTER:
return {
...state,
registered: false
};
default:
return state;
}
}
export const getRegistered = (state: State) => state.registered;
/**
* 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, createFeatureSelector } from '@ngrx/store';
import * as samp from './samp.reducer';
export const getSampState = createFeatureSelector<samp.State>('samp');
export const getRegistered = createSelector(
getSampState,
samp.getRegistered
);
/**
* 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 { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
declare var samp: any;
@Injectable()
/**
* @class
* @classdesc Samp service.
*/
export class SampService {
private connector = null;
constructor() {
const baseUrl = window.location.protocol + "//" + window.location.host + environment.baseHref;
const meta = {
"samp.name": "ANIS",
"samp.description.text": "AstroNomical Information System",
"author.email": "cesamsi@lam.fr",
"author.affiliation": "CeSAM, Laboratoire d'Astrophysique de Marseille",
"home.page": "https://anis.lam.fr",
"samp.icon.url": baseUrl + "/assets/cesam_anis40.png"
};
this.connector = new samp.Connector("anis-client", meta)
}
register() {
return new Observable(observer => {
samp.register(this.connector.name, (conn) => {
this.connector.setConnection(conn);
observer.next(true);
observer.complete();
}, () => {
observer.error(new Error('Oups!'));
observer.complete();
});
});
}
unregister(): void {
this.connector.unregister();
}
broadcast(mtype: string, url: string): void {
const message = new samp.Message(mtype, {"url": encodeURI(url)});
this.connector.connection.notifyAll([message]);
}
}
......@@ -5,31 +5,21 @@
<div class="w-100 d-block d-xl-none"></div>
<div class="col">
<div class="row justify-content-around">
<div *ngIf="getConfigDownloadResultFormat('csv')" class="col-auto mb-2">
<div *ngIf="dataset.config.download.csv" class="col-auto mb-2">
<a [href]="getUrl('csv')" class="btn btn-lg btn-block dl-btn">
<span [inlineSVG]="'assets/logo_csv.svg'" title="Download results in CSV format"></span>
</a>
</div>
<div *ngIf="getConfigDownloadResultFormat('ascii')" class="col-auto mb-2">
<div *ngIf="dataset.config.download.ascii" class="col-auto mb-2">
<a [href]="getUrl('ascii')" target="_blank" class="btn btn-lg btn-block dl-btn">
<span [inlineSVG]="'assets/logo_ascii.svg'" title="Download results in ASCII format"></span>
</a>
</div>
<div *ngIf="getConfigDownloadResultFormat('vo')" class="col-auto mb-2">
<div *ngIf="dataset.config.download.vo" class="col-auto mb-2">
<button class="btn btn-lg btn-block dl-btn">
<span [inlineSVG]="'assets/logo_vo.svg'" title="Download results in VO format"></span>
</button>
</div>
<div *ngIf="getConfigDownloadResultFormat('spectra')" class="col-auto mb-2">
<button class="btn btn-lg btn-block dl-btn">
<span [inlineSVG]="'assets/logo_spectra.svg'" title="Download SPECTRA archive"></span>
</button>
</div>
<div *ngIf="getConfigDownloadResultFormat('stamp')" class="col-auto mb-2">
<button class="btn btn-lg btn-block dl-btn">
<span [inlineSVG]="'assets/logo_stamp.svg'" title="Download STAMP archive"></span>
</button>
</div>
</div>
</div>
</div>
\ No newline at end of file
......@@ -21,13 +21,6 @@ describe('[SearchMultiple][Result] Component: DownloadSectionComponent', () => {
expect(component).toBeTruthy();
});
it('#getConfigDownloadResultFormat() should return if download button has to be displayed for the given format', () => {
component.dataset = DATASET;
expect(component.getConfigDownloadResultFormat('csv')).toBeTruthy();
component.dataset = DATASET_LIST.find(d => d.name === 'cat_2');
expect(component.getConfigDownloadResultFormat('csv')).toBeFalsy();
});
it('#getUrl(format) should construct url with format parameter', () => {
component.dataset = DATASET;
component.outputList = [1, 2, 3];
......
......@@ -28,20 +28,6 @@ export class DownloadSectionComponent {
@Input() coneSearch: ConeSearch;
@Input() outputList: number[];
/**
* Checks if configuration allowed downloading for the given format.
*
* @param {string} format - The format.
*
* @return boolean
*/
getConfigDownloadResultFormat(format: string): boolean {
if (this.dataset.config && this.dataset.config.results_format && this.dataset.config.results_format[format]) {
return this.dataset.config.results_format[format];
}
return false;
}
/**
* Returns URL to download data in the given format.
*
......
<accordion *ngIf="coneSearchEnabled()" [isAnimated]="true">
<accordion-group #ag [panelClass]="'custom-accordion'" [isOpen]="true" class="my-2">
<accordion-group #ag [panelClass]="'custom-accordion'" [isOpen]="coneSearchOpened()" class="my-2">
<button class="btn btn-link btn-block clearfix" accordion-heading>
<span class="pull-left float-left">
Cone search
......
......@@ -34,9 +34,16 @@ export class ConeSearchTabComponent {
*/
coneSearchEnabled(): boolean {
const config = this.datasetList.find(d => d.name === this.datasetName).config;
if (config !== null && 'cone_search' in config) {
return config.cone_search.enabled;
}
return false;
return config.cone_search.enabled;
}
/**
* Checks if accordion is opened.
*
* @return boolean
*/
coneSearchOpened(): boolean {
const config = this.datasetList.find(d => d.name === this.datasetName).config;
return config.cone_search.opened;
}
}
......@@ -18,6 +18,7 @@ import { ReminderComponent } from './result/reminder.component';
import { UrlDisplaySectionComponent } from './result/url-display.component';
import { DatatableTabComponent } from './result/datatable-tab.component';
import { ConeSearchPlotTabComponent } from './result/cone-search-plot-tab.component';
import { SampComponent } from './result/samp.component';
export const dummiesComponents = [
ProgressBarComponent,
......@@ -39,5 +40,6 @@ export const dummiesComponents = [
ReminderComponent,
UrlDisplaySectionComponent,
DatatableTabComponent,
ConeSearchPlotTabComponent
ConeSearchPlotTabComponent,
SampComponent
];
<accordion *ngIf="dataLength > 0 && dataLengthIsLoaded && checkDisplayPlot() && isConeSearchAdded" [isAnimated]="true">
<accordion-group #ag [isOpen]="isDatatableOpened()" [panelClass]="'custom-accordion'" class="my-2">
<accordion-group #ag [isOpen]="isConeSearchOpened()" [panelClass]="'custom-accordion'" class="my-2">
<button class="btn btn-link btn-block clearfix" accordion-heading>
<span class="pull-left float-left">
Cone-search plot
......
......@@ -32,13 +32,9 @@ export class ConeSearchPlotTabComponent {
*
* @return boolean
*/
isDatatableOpened(): boolean {
isConeSearchOpened(): boolean {
const config = this.getDataset().config;
config.result_page_modules.opened_datatable;
if (config && config.result_page_modules) {
return config.result_page_modules.opened_datatable;
}
return false;
return config.cone_search.opened;
}
/**
......
<accordion *ngIf="dataLength > 0 && _dataLengthIsLoaded" [isAnimated]="true">
<accordion *ngIf="dataLength > 0 && _dataLengthIsLoaded && isDatatableEnabled()" [isAnimated]="true">
<accordion-group #ag [isOpen]="isDatatableOpened()" [panelClass]="'custom-accordion'" class="my-2">
<button class="btn btn-link btn-block clearfix" accordion-heading>
<span class="pull-left float-left">
......
......@@ -59,6 +59,11 @@ export class DatatableTabComponent {
_dataLengthIsLoaded: boolean = false;
isDatatableEnabled(): boolean {
const config = this.getDataset().config;
return config.datatable.enabled;
}
/**
* Checks if datatable accordion should be open.
*
......@@ -66,11 +71,7 @@ export class DatatableTabComponent {
*/
isDatatableOpened(): boolean {
const config = this.getDataset().config;
config.result_page_modules.opened_datatable;
if (config && config.result_page_modules) {
return config.result_page_modules.opened_datatable;
}
return false;
return config.datatable.opened;
}
/**
......
......@@ -2,46 +2,55 @@
<div class="lead">
Dataset <span class="bold">{{ getDatasetLabel() }}</span> selected with <span class="bold">{{ dataLength }}</span> objects found.
</div>
<hr *ngIf="isDownloadActivated()" class="my-4">
<div *ngIf="isDownloadActivated()" class="row">
<div class="col-auto align-self-center">
<p>Download results just here:</p>
</div>
<div class="w-100 d-block d-xl-none"></div>
<div class="col">
<div class="row justify-content-around">
<div *ngIf="getConfigDownloadResultFormat('csv')" class="col-auto mb-2">
<a [href]="getUrl('csv')" class="btn btn-lg btn-block dl-btn">
<span [inlineSVG]="'assets/logo_csv.svg'" title="Download results in CSV format"></span>
</a>
</div>
<accordion *ngIf="dataLengthIsLoaded && isDownloadActivated()" [isAnimated]="true">
<accordion-group #ag [isOpen]="isDownloadOpened()" [panelClass]="'custom-accordion'" class="my-2">
<button class="btn btn-link btn-block clearfix" accordion-heading>
<span class="pull-left float-left">
Download results
&nbsp;
<span *ngIf="ag.isOpen">
<span class="fas fa-chevron-up"></span>
</span>
<span *ngIf="!ag.isOpen">
<span class="fas fa-chevron-down"></span>
</span>
</span>
</button>
<div>
<div class="row">
<div class="col-auto align-self-center">
<p>Download results just here:</p>
</div>
<div *ngIf="getConfigDownloadResultFormat('ascii')" class="col-auto mb-2">
<a [href]="getUrl('ascii')" target="_blank" class="btn btn-lg btn-block dl-btn">
<span [inlineSVG]="'assets/logo_ascii.svg'" title="Download results in ASCII format"></span>