Commit a25bf4d6 authored by Tifenn Guillas's avatar Tifenn Guillas
Browse files

Merge branch 'develop' into 'master'

Develop

See merge request !147
parents 67431bab d0bbff78
......@@ -4,12 +4,49 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.4.0] - 2020-10
### Added
- #132 => Add reset cone search button in search multiple
- #122 => Add download results buttons to search multiple
- #97 => Add a new search multiple module
- #21 => Add module lazy loading
- #109 => Add help tooltip to like and not like operators
- #112 => Add domain names on documentation urls
### Fixed
- #116 => Fix search type list placeholder
- #115 => Fix between criterion that didn't fill correctly on page reload
- #113: Fix output list didn't change on summary box
- #111: Fix search lost when back to dataset page
### Changed
- #129 => By default in search multiple there is no selected dataset
- #130 => Sort datasets in search multiple by dataset family rather than project
- #126 => Download results in different formats are now a configurable option in anis-admin
- #124 => Navigation links are now a configurable option in anis-admin
- #125 => Display datasets with cone search only on search multiple
- #118 => Refactoring metamodel
- #81 => Opened result datatable is now a configurable option in anis-admin
- #81 => Display 'Direct link to the result' is now a configurable option in anis-admin
- #106 => Design update for summary box in result page
- #114 => Design update for outputs in summary box
- #110 => Design update for detail page
- #107 => Change select box for cone search to input text
- #99 => Improve cone search design for small screens and mobiles
- #100 => Improve outputs design on search summary
## [3.3.0] - 2020-06
### Added
- #88 => Search by cone search
- #89 => Name resolver added to cone search
- #90 => Dynamic documentation to explain how to export results via server urls
- #95 => Search by list
- #109 => Add tooltips on like and not like operators to help user
- #21 => Add lazy loading for features
- #81 => Section `Direct link to the result` as configurable option on result page
- #66 => Folded / unfolded datatable section as configurable option on result page
- #97 => Add multiple search
### Changed
- #91 => Download results buttons colors can be changed
......@@ -17,6 +54,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #93 => Auto select dataset if only one present
- #94 => Home redirection to project base href
- #96 => Improve search summary design
- #107 => Change field design for cone search hh:mm:ss coordinates
- #110 => Change design outputs on detail page
- #114 => Change design of outputs summary on result page for better readability
- #106 => Change summary design on result page
- #6 => README file more detailed
### Fixed
- #111 => Fix bug search lost when back to datasets page
- #113 => Fix bug output list not updated
- #115 => Fix bug on between criterion
- #116 => Fix bug placeholder for list criterion
## [3.2.0] - 2020-04
......
# ANIS CLIENT
# ANIS-CLIENT
## Introduction
AstroNomical Information System est un outil web générique qui vise à faciliter
la mise à disposition de données (Astrophysiques), accessible depuis
une base de données, à une communauté de scientifiques.
AstroNomical Information System, ou ANIS, est une suite d'outils web génériques qui visent à faciliter la mise à
disposition de données astrophysiques, accessibles depuis une base de données, à une communauté de scientifiques.
ANIS-CLIENT est un composant de la suite ANIS qui permet de visualiser les datasets à travers un navigateur web.
A partir des informations contenues dans la base de metamodel, ANIS-CLIENT est capable :
- d'afficher la liste des datasets disponibles pour une instance donnée.
- de générer un formulaire de recherche sur un dataset en sélectionnant des critères ainsi que les résultats à afficher.
- de visualiser et télécharger les résultats de la recherche et informations associées.
- d'afficher les informations détaillées d'un objet à travers des graphes ou des images par exemple.
ANIS est protégée par la licence CeCILL (voir le fichier [LICENCE](LICENCE)).
## Installation
### Prérequis
Avant de commencer l'installation, assurez-vous d'avoir les commandes suivantes installées sur votre système :
- `make`
- `docker`
Une connexion internet sera nécessaire afin de télécharger les dépendances.
### Installation et démarrage de l'application en mode développement
ANIS-CLIENT contient un Makefile qui aide à l'installation et au démarrage de l'application.
1. Pour installer l'application et ces dépendances, taper à la racine du projet :
> make install
2. Lancer l'application :
> make start
3. Stopper l'application :
> make stop
**Attention** : ANIS-CLIENT repose sur ANIS-SERVER pour fonctionner !
### Liste des commandes
Le fichier `Makefile` situé à la racine du projet rend disponible une liste de commandes utiles à la gestion de
l'application en mode développement.
Pour voir la liste des commandes disponibles, ouvrir un terminal à la racine du projet et taper :
> make
**Attention** : ces commandes sont destinées à être utilisées en mode développement uniquement !
### Mode production
Pour compiler le projet pour de la production, il existe une commande make pour générer le dossier `dist` à la racine :
> make build
## Technologies
Voici une liste non exhaustive des principales dépendances utilisées lors du développement d'ANIS-CLIENT :
* `Angular` : [https://angular.io](https://angular.io/)
* `NGRX` : [https://ngrx.io](https://ngrx.io/)
* `Bootstrap` : [https://getbootstrap.com](https://getbootstrap.com)
* `D3.js` : [https://d3js.org](https://d3js.org)
* `Docker` : [https://www.docker.com](https://www.docker.com)
* `GIT` : [http://git-scm.com](http://git-scm.com)
* `CeCILL`: [http://www.cecill.info/index.en.html](http://www.cecill.info/index.en.html)
ANIS-CLIENT est un composant de ANIS qui permet de visualiser les datasets dans un web browser.
ANIS-CLIENT est capable de générer des formulaires et d'interroger une ou plusieurs bases de données
à partir des informations contenues dans la base de metamodel.
## Auteurs
* `François Agneray` : Laboratoire d'Astrophysique de Marseille (CNRS)
* `Chrystel Moreau` : Laboratoire d'Astrophysique de Marseille (CNRS)
\ No newline at end of file
* `Chrystel Moreau` : Laboratoire d'Astrophysique de Marseille (CNRS)
* `Tifenn Guillas` : Laboratoire d'Astrophysique de Marseille (CNRS)
\ No newline at end of file
3.3.0
\ No newline at end of file
3.4.0
\ No newline at end of file
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
import { NotFoundPageComponent } from './core/containers/not-found-page.component';
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{
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: 'detail/:dname/:objectSelected',
loadChildren: () => import('./detail/detail.module').then(m => m.DetailModule)
},
{
path: 'documentation',
loadChildren: () => import('./documentation/documentation.module').then(m => m.DocumentationModule)
},
{ path: '**', component: NotFoundPageComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
],
exports: [RouterModule]
})
export class AppRoutingModule { }
......
......@@ -13,10 +13,8 @@ import { reducers, metaReducers } from './app.reducer';
import { CoreModule } from './core/core.module';
import { StaticModule } from './static/static.module';
import { LoginModule } from './login/login.module';
import { SearchModule } from './search/search.module';
import { DetailModule } from './detail/detail.module';
import { DocumentationModule } from './documentation/documentation.module';
import { AppRoutingModule } from './app.routing';
import { MetamodelModule } from './metamodel/metamodel.module';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './core/containers/app.component';
import { environment } from '../environments/environment';
......@@ -28,9 +26,7 @@ import { environment } from '../environments/environment';
CoreModule,
StaticModule,
LoginModule,
SearchModule,
DetailModule,
DocumentationModule,
MetamodelModule,
StoreModule.forRoot(reducers, {
metaReducers,
runtimeChecks: {
......
import { ActionReducerMap, ActionReducer, MetaReducer } from '@ngrx/store';
import { environment } from '../environments/environment';
import * as fromRouter from '@ngrx/router-store';
import { environment } from '../environments/environment';
export interface State {
router: fromRouter.RouterReducerState;
}
......
......@@ -12,12 +12,17 @@
<span class="fas fa-home"></span> Home
</a>
</li>
<li class="nav-item">
<li *ngIf="isSearchAllowed()" id="search_link" class="nav-item">
<a class="nav-link" routerLink="/search" routerLinkActive="active">
<span class="fas fa-search"></span> Search
</a>
</li>
<li class="nav-item">
<li *ngIf="isSearchMultipleAllowed()" id="search_multiple_link" class="nav-item">
<a class="nav-link" routerLink="/search-multiple" routerLinkActive="active">
<span class="fas fa-search-plus"></span> Search multiple
</a>
</li>
<li *ngIf="isDocumentationAllowed()" id="documentation_link" class="nav-item">
<a class="nav-link" routerLink="/documentation" routerLinkActive="active">
<span class="fas fa-question"></span> Documentation
</a>
......@@ -74,12 +79,17 @@
<span class="fas fa-home fa-fw"></span> Home
</a>
</li>
<li role="menuitem">
<li *ngIf="isSearchAllowed()" role="menuitem">
<a class="dropdown-item" routerLink="/search">
<span class="fas fa-search fa-fw"></span> Search
</a>
</li>
<li role="menuitem">
<li *ngIf="isSearchMultipleAllowed()" role="menuitem">
<a class="dropdown-item" routerLink="/search-multiple">
<span class="fas fa-search-plus fa-fw"></span> Search multiple
</a>
</li>
<li *ngIf="isDocumentationAllowed()" role="menuitem">
<a class="dropdown-item" routerLink="/documentation">
<span class="fas fa-question fa-fw"></span> Documentation
</a>
......
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NavComponent } from './nav.component';
......@@ -49,5 +49,130 @@ describe('[Core] Component: NavComponent', () => {
it('raises the logout event when clicked', () => {
component.logout.subscribe((event) => expect(event).toBe(undefined));
component.emitLogout();
});
});
it('should display search, search multiple and documentation links if instance config allows it', () => {
component.instance = {
name: 'toto',
label: 'Toto',
client_url: '',
nb_dataset_families: 1,
nb_datasets: 1,
config: {
search: true,
search_multiple: true,
documentation: true
}
};
fixture.detectChanges();
const template = fixture.nativeElement;
expect(template.querySelector('#search_link')).toBeTruthy();
expect(template.querySelector('#search_multiple_link')).toBeTruthy();
expect(template.querySelector('#documentation_link')).toBeTruthy();
});
it('should not display search, search multiple and documentation links if instance config don\'t allows it', () => {
component.instance = {
name: 'toto',
label: 'Toto',
client_url: '',
nb_dataset_families: 1,
nb_datasets: 1,
config: {
search: false,
search_multiple: false,
documentation: false
} };
fixture.detectChanges();
const template = fixture.nativeElement;
expect(template.querySelector('#search_link')).toBeFalsy();
expect(template.querySelector('#search_multiple_link')).toBeFalsy();
expect(template.querySelector('#documentation_link')).toBeFalsy();
});
it('#isSearchAllowed() return if search link has to be displayed', () => {
component.instance = {
name: 'toto',
label: 'Toto',
client_url: '',
nb_dataset_families: 1,
nb_datasets: 1,
config: {
search: true,
search_multiple: false,
documentation: false
}
};
expect(component.isSearchAllowed()).toBeTruthy();
component.instance = {
name: 'toto',
label: 'Toto',
client_url: '',
nb_dataset_families: 1,
nb_datasets: 1,
config: {
search: false,
search_multiple: false,
documentation: false
} };
expect(component.isSearchAllowed()).toBeFalsy();
});
it('#isSearchMultipleAllowed() return if search multiple link has to be displayed', () => {
component.instance = {
name: 'toto',
label: 'Toto',
client_url: '',
nb_dataset_families: 1,
nb_datasets: 1,
config: {
search: true,
search_multiple: true,
documentation: false
}
};
expect(component.isSearchMultipleAllowed()).toBeTruthy();
component.instance = {
name: 'toto',
label: 'Toto',
client_url: '',
nb_dataset_families: 1,
nb_datasets: 1,
config: {
search: true,
search_multiple: false,
documentation: false
}
};
expect(component.isSearchMultipleAllowed()).toBeFalsy();
});
it('#isDocumentationAllowed() return if documentation link has to be displayed', () => {
component.instance = {
name: 'toto',
label: 'Toto',
client_url: '',
nb_dataset_families: 1,
nb_datasets: 1,
config: {
search: true,
search_multiple: true,
documentation: true
}
};
expect(component.isDocumentationAllowed()).toBeTruthy();
component.instance = {
name: 'toto',
label: 'Toto',
client_url: '',
nb_dataset_families: 1,
nb_datasets: 1,
config: {
search: true,
search_multiple: true,
documentation: false
}
};
expect(component.isDocumentationAllowed()).toBeFalsy();
});
});
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
import { LoginToken } from '../../login/store/model';
import { Instance } from '../../metamodel/model';
import { environment } from '../../../environments/environment'
@Component({
......@@ -12,10 +13,31 @@ import { environment } from '../../../environments/environment'
export class NavComponent {
@Input() isAuthenticated: boolean;
@Input() loginToken: LoginToken;
@Input() instance: Instance;
@Output() logout: EventEmitter<any> = new EventEmitter();
isCollapsed = true;
baseHref: string = environment.baseHref;
isSearchAllowed(): boolean {
if (this.instance && this.instance.config.search) {
return this.instance.config.search;
}
return false;
}
isSearchMultipleAllowed(): boolean {
if (this.instance && this.instance.config.search_multiple) {
return this.instance.config.search_multiple;
}
return false;
}
isDocumentationAllowed(): boolean {
if (this.instance && this.instance.config.documentation) {
return this.instance.config.documentation;
}
return false;
}
emitLogout() {
this.logout.emit();
}
......
<header>
<app-nav [isAuthenticated]="isAuthenticated | async" [loginToken]="loginToken | async" (logout)="logout()">
<app-nav
[isAuthenticated]="isAuthenticated | async"
[loginToken]="loginToken | async"
[instance]="instance | async"
(logout)="logout()">
</app-nav>
</header>
<main role="main" class="container-fluid pb-4">
......
import { Component, Input } from '@angular/core';
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { provideMockStore, MockStore } from '@ngrx/store/testing';
import { AppComponent } from './app.component';
import * as fromLogin from '../../login/store/login.reducer';
import * as loginActions from '../../login/store/login.action';
import { LoginToken } from '../../login/store/model';
import * as fromMetamodel from '../../metamodel/reducers';
import * as instanceActions from '../../metamodel/action/instance.action';
describe('[Core] Container: AppComponent', () => {
@Component({ selector: 'app-nav', template: '' })
class NavStubComponent {
@Input() isAuthenticated: boolean;
@Input() loginToken: LoginToken;
}
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let store: MockStore;
const initialState = {
login: { ...fromLogin.initialState },
metamodel: { ...fromMetamodel }
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [
AppComponent,
NavStubComponent
],
providers: [
provideMockStore({ initialState })
]
});
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
store = TestBed.inject(MockStore);
}));
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should execute ngOnInit lifecycle', () => {
const loginLocalStorageAction = new loginActions.LoginLocalStorageAction();
const loadInstanceMetaAction = new instanceActions.LoadInstanceMetaAction();
const spy = spyOn(store, 'dispatch');
component.ngOnInit();
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenCalledWith(loginLocalStorageAction);
expect(spy).toHaveBeenCalledWith(loadInstanceMetaAction);
});
it('#logout() should dispatch LogoutAction', () => {
const logoutAction = new loginActions.LogoutAction();
const spy = spyOn(store, 'dispatch');
component.logout();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(logoutAction);
});
});
......@@ -3,12 +3,21 @@ import { Component, ViewEncapsulation, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { LoginToken } from '../../login/store/model';
import * as fromLogin from '../../login/store/login.reducer';
import * as loginActions from '../../login/store/login.action';
import * as loginReducer from '../../login/store/login.reducer';
import * as loginSelector from '../../login/store/login.selector';
import { LoginToken } from '../../login/store/model';
import * as fromMetamodel from '../../metamodel/reducers';
import * as instanceActions from '../../metamodel/action/instance.action';
import * as metamodelSelector from '../../metamodel/selectors';
import { Instance } from '../../metamodel/model';
import { VERSIONS } from '../../../settings/settings';
interface StoreState {
login: fromLogin.State;
metamodel: fromMetamodel.State;
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
......@@ -16,18 +25,21 @@ import { VERSIONS } from '../../../settings/settings';
encapsulation: ViewEncapsulation.None
})
export class AppComponent implements OnInit {
anisClientVersion: string = VERSIONS.anisClient;
year = (new Date()).getFullYear();
isAuthenticated: Observable<boolean>;
loginToken: Observable<LoginToken>;
public anisClientVersion: string = VERSIONS.anisClient;
public year = (new Date()).getFullYear();
public isAuthenticated: Observable<boolean>;
public loginToken: Observable<LoginToken>;
public instance: Observable<Instance>;
constructor(private store: Store<loginReducer.State>) {
constructor(private store: Store<StoreState>) {
this.isAuthenticated = store.select(loginSelector.isAuthenticated);
this.loginToken = store.select(loginSelector.getLoginToken);
this.instance = store.select(metamodelSelector.getInstance);
}
ngOnInit() {
this.store.dispatch(new loginActions.LoginLocalStorageAction());
this.store.dispatch(new instanceActions.LoadInstanceMetaAction());
}
logout(): void {
......
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotFoundPageComponent } from './not-found-page.component';
......
......@@ -8,9 +8,15 @@
</div>
<div [ngClass]="{'col-md-4 col-sm-12': getAttributeSpectraGraph()}" class="col mt-4">