Commit 67431bab authored by François Agneray's avatar François Agneray

Merge branch 'develop' into 'master'

Develop

See merge request !113
parents cc53af32 c2446847
......@@ -6,7 +6,7 @@ stages:
- deploy
variables:
VERSION: "3.1"
VERSION: "3.3.0"
SONARQUBE_URL: https://sonarqube.lam.fr
CONTAINER_IMAGE: portus.lam.fr/anis/anis-client
COVERAGE_IMAGE: portus.lam.fr/anis/anis-client-coverage
......
......@@ -4,6 +4,21 @@ 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.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
### Changed
- #91 => Download results buttons colors can be changed
- #92 => Improvement of sonarqube quality
- #93 => Auto select dataset if only one present
- #94 => Home redirection to project base href
- #96 => Improve search summary design
## [3.2.0] - 2020-04
### Added
- #69 => User can change the number of object displayable (10, 20, 50, 100) in result datatable
......
3.1
\ No newline at end of file
3.3.0
\ No newline at end of file
......@@ -15,6 +15,7 @@ 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 { AppComponent } from './core/containers/app.component';
import { environment } from '../environments/environment';
......@@ -29,6 +30,7 @@ import { environment } from '../environments/environment';
LoginModule,
SearchModule,
DetailModule,
DocumentationModule,
StoreModule.forRoot(reducers, {
metaReducers,
runtimeChecks: {
......@@ -47,7 +49,6 @@ import { environment } from '../environments/environment';
!environment.production ? StoreDevtoolsModule.instrument() : [],
EffectsModule.forRoot([])
],
// providers: [{ provide: RouterStateSerializer, useClass: CustomRouterStateSerializer }],
bootstrap: [AppComponent]
})
export class AppModule { }
<nav class="navbar navbar-light bg-light navbar-expand-md fixed-top border-bottom">
<!-- Logo -->
<a href="/" class="navbar-brand">
<a href="{{ baseHref }}" class="navbar-brand">
<img src="assets/cesam_anis80.png" alt="CeSAM logo" />
</a>
......@@ -17,6 +17,11 @@
<span class="fas fa-search"></span> Search
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="/documentation" routerLinkActive="active">
<span class="fas fa-question"></span> Documentation
</a>
</li>
</ul>
<button *ngIf="!isAuthenticated" class="btn btn-outline-success my-2 my-sm-0"
id="button-sign-in"
......@@ -74,6 +79,11 @@
<span class="fas fa-search fa-fw"></span> Search
</a>
</li>
<li role="menuitem">
<a class="dropdown-item" routerLink="/documentation">
<span class="fas fa-question fa-fw"></span> Documentation
</a>
</li>
<li *ngIf="isAuthenticated" class="divider dropdown-divider"></li>
<li *ngIf="isAuthenticated" role="menuitem">
<a class="dropdown-item" routerLink="/change-password">
......
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
import { LoginToken } from '../../login/store/model';
import { environment } from '../../../environments/environment'
@Component({
selector: 'app-nav',
......@@ -13,6 +14,7 @@ export class NavComponent {
@Input() loginToken: LoginToken;
@Output() logout: EventEmitter<any> = new EventEmitter();
isCollapsed = true;
baseHref: string = environment.baseHref;
emitLogout() {
this.logout.emit();
......
......@@ -70,8 +70,7 @@
}
.text-tooltip {
font-size: 13px;
font-family: "Segoe UI";
font-size: 15px;
color: #333333;
fill: #333333;
}
......
......@@ -31,7 +31,7 @@ export function reducer(state: State = initialState, action: actions.Actions): S
};
case actions.LOAD_ATTRIBUTE_LIST_SUCCESS:
const attributeList = action.payload as Attribute[];
const attributeList: Attribute[] = action.payload;
return {
...state,
......
blockquote {
background: #f9f9f9;
border-left: 10px solid #ccc;
margin: 1.5em 10px;
padding: 1em 10px 1em 10px;
}
table, th, td {
background-color: #f9f9f9;
padding: 6px 13px;
border: 1px solid #ccc;
border-collapse: collapse;
margin-bottom: 1em;
}
<div class="container">
<div class="jumbotron">
<div class="row align-items-center">
<div class="col-md-12 order-md-1 text-justify text-md-left pr-md-5">
<h2 class="mb-3">Export server documentation</h2>
<h4>URL construction</h4>
<p>
To request the server, you need to construct a correct URL. Just below you can find the URL schema and a
description of mandatory parameters:
</p>
<code>{{ apiPath }}/search/dataset?a=id_attribute&c=id_attribute::operator::value</code>
<ul>
<li>
<code>dataset</code>: dataset in which to search. See datasets section for available datasets.
</li>
<li>
<code>a</code>: output parameters as attributes id list semicolon separated. See outputs section for available attributes.
</li>
<blockquote>a=1;2;3</blockquote>
<li>
<code>c</code>: criteria list separeted with semicolon. A criterion is defined by an id_attribute,
an operator and a value. See operators section for available operators.
</li>
<blockquote>c=3::eq::ping;2::eq::pong</blockquote>
</ul>
<h4>Available parameters</h4>
<h5>Datasets</h5>
<div *ngIf="datasetListIsLoading | async">
<span class="fas fa-circle-notch fa-spin fa-3x"></span>
<span class="sr-only">Loading...</span>
</div>
<table *ngIf="datasetListIsLoaded | async" id="table">
<tr>
<th>Dataset</th>
<th>Description</th>
</tr>
<tr *ngFor="let dataset of datasetList | async">
<td>{{ dataset.name }}</td>
<td>{{ dataset.description }}</td>
</tr>
</table>
<h5>Outputs</h5>
<div *ngIf="attributeListIsLoading | async">
<span class="fas fa-circle-notch fa-spin fa-3x"></span>
<span class="sr-only">Loading...</span>
</div>
<div *ngIf="attributeListIsLoaded | async" class="row">
<div *ngFor="let dataset of datasetList | async" class="col-auto">
<h6>{{ dataset.label }} output list</h6>
<table id="table">
<tr>
<th>id</th>
<th>attribute</th>
</tr>
<tr *ngFor="let attribute of getDatasetAttributes(dataset, attributeList | async)">
<td>{{ attribute.id }}</td>
<td>{{ attribute.name }}</td>
</tr>
</table>
</div>
</div>
<h5>Operators</h5>
<table id="table">
<tr>
<th>operator</th>
<th>description</th>
<th>usage</th>
<th>example</th>
</tr>
<tr>
<td>eq</td>
<td>equal to</td>
<td><code>c=id_attribute::eq::value</code></td>
<td><code>c=1::eq::89</code></td>
</tr>
<tr>
<td>neq</td>
<td>not equal to</td>
<td><code>c=id_attribute::neq::value</code></td>
<td><code>c=1::neq::89</code></td>
</tr>
<tr>
<td>gt</td>
<td>greater than</td>
<td><code>c=id_attribute::gt::value</code></td>
<td><code>c=1::gt::1.5</code></td>
</tr>
<tr>
<td>gte</td>
<td>greater than or equal to</td>
<td><code>c=id_attribute::gte::value</code></td>
<td><code>c=1::gte::2</code></td>
</tr>
<tr>
<td>lt</td>
<td>lower than</td>
<td><code>c=id_attribute::lt::value</code></td>
<td><code>c=1::lt::1.5</code></td>
</tr>
<tr>
<td>lte</td>
<td>lower than or equal to</td>
<td><code>c=id_attribute::lte::value</code></td>
<td><code>c=1::lte::2</code></td>
</tr>
<tr>
<td>bw</td>
<td>between</td>
<td><code>c=id_attribute::bw::value_min|value_max</code></td>
<td><code>c=1::bw::10|90</code></td>
</tr>
<tr>
<td>lk</td>
<td>like</td>
<td><code>c=id_attribute::lk::value</code></td>
<td><code>c=1::lk::ping</code></td>
</tr>
<tr>
<td>nlk</td>
<td>not like</td>
<td><code>c=id_attribute::nlk::value</code></td>
<td><code>c=1::nlk::pong</code></td>
</tr>
<tr>
<td>in</td>
<td>in</td>
<td><code>c=id_attribute::in::value_x|value_y|value_z</code></td>
<td><code>c=1::in::ping|pong|paff</code></td>
</tr>
<tr>
<td>nin</td>
<td>not in</td>
<td><code>c=id_attribute::nin::value_x|value_y|value_z</code></td>
<td><code>c=1::nin::ping|pong|paf</code></td>
</tr>
<tr>
<td>nl</td>
<td>is null</td>
<td><code>c=id_attribute::nl</code></td>
<td><code>c=1::nl</code></td>
</tr>
<tr>
<td>nnl</td>
<td>is not null</td>
<td><code>c=id_attribute::nnl</code></td>
<td><code>c=1::nnl</code></td>
</tr>
<tr>
<td>js</td>
<td>json</td>
<td><code>c=id_attribute::js::extension,keyword|operator|value</code></td>
<td><code>c=1::js::PrimaryHDU,ID|eq|45</code></td>
</tr>
</table>
<h4>Examples</h4>
We supposed to have the dataset ping with following attributes:
<table id="table">
<tr>
<th>id</th>
<th>attribute</th>
</tr>
<tr>
<td>1</td>
<td>obs_id</td>
</tr>
<tr>
<td>2</td>
<td>ra</td>
</tr>
<tr>
<td>3</td>
<td>dec</td>
</tr>
<tr>
<td>4</td>
<td>instrument</td>
</tr>
</table>
<blockquote>{{ apiPath }}/search/ping?a=1;2;3&c=1::eq::1</blockquote>
<p>This will return the <code>obs_id</code> with its value equals to 1 and display <code>obs_id</code>, <code>RA</code> and <code>DEC</code> as
outputs.</p>
<blockquote>
{{ apiPath }}/search/ping?a=1;2;3;4&c=4::in::TEL_1|TEL_2
</blockquote>
<p>This will return a list of <code>TEL_1</code> or <code>TEL_2</code> observations with all available
outputs.</p>
<blockquote>
{{ apiPath }}/search/ping?a=1&c=2::gt::1;3::gt::2
</blockquote>
<p>This will return a list of <code>obs_id</code> where <code>RA</code> is greater than 1 and <code>DEC</code> is greater than 2</p>
</div>
</div>
</div>
</div>
\ No newline at end of file
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideMockStore, MockStore } from '@ngrx/store/testing';
import { DocumentationComponent } from './documentation.component';
import * as fromDocumentation from '../store/documentation.reducer';
import * as documentationActions from '../store/documentation.action';
import { DATASET, ATTRIBUTE_LIST } from 'src/settings/test-data';
import { Dataset, Attribute } from 'src/app/metamodel/model';
describe('[Documentation] Component: DocumentationComponent', () => {
let component: DocumentationComponent;
let fixture: ComponentFixture<DocumentationComponent>;
let store: MockStore;
const initialState = { documentation: { ...fromDocumentation.initialState }};;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ DocumentationComponent ],
providers: [ provideMockStore({ initialState }) ]
});
fixture = TestBed.createComponent(DocumentationComponent);
component = fixture.componentInstance;
store = TestBed.inject(MockStore);
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('#getDatasetAttributes() should return attributes for selected dataset', () => {
const dataset: Dataset = DATASET;
const attributeList: Attribute[] = ATTRIBUTE_LIST;
const expectedAttributeList = attributeList.filter(a => a.table_name === dataset.table_ref);
expect(component.getDatasetAttributes(dataset, attributeList)).toEqual(expectedAttributeList);
});
it('should execute ngOnInit lifecycle', () => {
const action = new documentationActions.RetrieveDatasetList();
const spy = spyOn(store, 'dispatch');
fixture.detectChanges();
expect(spy).toHaveBeenCalledWith(action);
});
});
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Store } from '@ngrx/store';
import * as documentationActions from '../store/documentation.action';
import * as fromDocumentation from '../store/documentation.reducer';
import * as documentationSelector from '../store/documentation.selector';
import { Dataset, Attribute } from 'src/app/metamodel/model';
import { environment } from '../../../environments/environment';
@Component({
selector: 'app-documentation',
templateUrl: 'documentation.component.html',
styleUrls: ['documentation.component.css']
})
export class DocumentationComponent implements OnInit {
public apiPath: string = environment.apiUrl;
public datasetListIsLoading: Observable<boolean>;
public datasetListIsLoaded: Observable<boolean>;
public datasetList: Observable<Dataset[]>;
public attributeListIsLoading: Observable<boolean>;
public attributeListIsLoaded: Observable<boolean>;
public attributeList: Observable<Attribute[]>;
constructor(private store: Store<{ documentation: fromDocumentation.State }>) {
this.datasetListIsLoading = store.select(documentationSelector.getDatasetListIsLoading);
this.datasetListIsLoaded = store.select(documentationSelector.getDatasetListIsLoaded);
this.datasetList = store.select(documentationSelector.getDatasetList);
this.attributeListIsLoading = store.select(documentationSelector.getAttributeListIsLoading);
this.attributeListIsLoaded = store.select(documentationSelector.getAttributeListIsLoaded);
this.attributeList = store.select(documentationSelector.getAttributeList);
}
ngOnInit() {
this.store.dispatch(new documentationActions.RetrieveDatasetList());
}
getDatasetAttributes(dataset: Dataset, attributeList: Attribute[]): Attribute[] {
return attributeList
.filter(attribute => attribute.table_name === dataset.table_ref)
.sort((a, b) => a.id - b.id);
}
}
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { SharedModule } from '../shared/shared.module';
import { DocumentationRoutingModule, routedComponents } from './documentation.routing';
import { reducer } from './store/documentation.reducer';
import { DocumentationEffects } from './store/documentation.effects';
import { DocumentationService } from './store/documentation.service';
@NgModule({
imports: [
SharedModule,
DocumentationRoutingModule,
StoreModule.forFeature('documentation', reducer),
EffectsModule.forFeature([ DocumentationEffects ])
],
declarations: [
routedComponents
],
providers: [
DocumentationService
]
})
export class DocumentationModule { }
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { DocumentationComponent } from './containers/documentation.component';
const routes: Routes = [
{ path: 'documentation', component: DocumentationComponent }
];
@NgModule({
imports: [ RouterModule.forChild(routes) ],
exports: [ RouterModule ]
})
export class DocumentationRoutingModule { }
export const routedComponents = [
DocumentationComponent
];
import * as documentationActions from './documentation.action';
import { Dataset, Attribute } from 'src/app/metamodel/model';
import { DATASET_LIST, ATTRIBUTE_LIST } from 'src/settings/test-data';
describe('[Documentation] Action', () => {
it('should create RetrieveDatasetList action', () => {
const action = new documentationActions.RetrieveDatasetList();
expect(action.type).toEqual(documentationActions.RETRIEVE_DATASET_LIST);
});
it('should create RetrieveDatasetListWip action', () => {
const action = new documentationActions.RetrieveDatasetListWip();
expect(action.type).toEqual(documentationActions.RETRIEVE_DATASET_LIST_WIP);
});
it('should create RetrieveDatasetListSuccess action', () => {
const datasetList: Dataset[] = DATASET_LIST;
const action = new documentationActions.RetrieveDatasetListSuccess(datasetList);
expect(action.type).toEqual(documentationActions.RETRIEVE_DATASET_LIST_SUCCESS);
expect(action.payload).toEqual(datasetList);
});
it('should create RetrieveDatasetListFail action', () => {
const action = new documentationActions.RetrieveDatasetListFail();
expect(action.type).toEqual(documentationActions.RETRIEVE_DATASET_LIST_FAIL);
});
it('should create RetrieveAttributeList action', () => {
const action = new documentationActions.RetrieveAttributeList(['toto']);
expect(action.type).toEqual(documentationActions.RETRIEVE_ATTRIBUTE_LIST);
expect(action.payload).toEqual(['toto']);
});
it('should create RetrieveAttributeListSuccess action', () => {
const attributeList: Attribute[] = ATTRIBUTE_LIST;
const action = new documentationActions.RetrieveAttributeListSuccess(attributeList);
expect(action.type).toEqual(documentationActions.RETRIEVE_ATTRIBUTE_LIST_SUCCESS);
expect(action.payload).toEqual(attributeList);
});
it('should create RetrieveAttributeListFail action', () => {
const action = new documentationActions.RetrieveAttributeListFail();
expect(action.type).toEqual(documentationActions.RETRIEVE_ATTRIBUTE_LIST_FAIL);
});
});
import { Action } from '@ngrx/store';
import { Dataset, Attribute } from 'src/app/metamodel/model';
export const RETRIEVE_DATASET_LIST = '[Documentation] Retrieve Dataset List';
export const RETRIEVE_DATASET_LIST_WIP = '[Documentation] Retrieve Dataset List WIP';
export const RETRIEVE_DATASET_LIST_SUCCESS = '[Documentation] Retrieve Dataset List Success';
export const RETRIEVE_DATASET_LIST_FAIL = '[Documentation] Retrieve Dataset List Fail';
export const RETRIEVE_ATTRIBUTE_LIST = '[Documentation] Retrieve Attribute List';
export const RETRIEVE_ATTRIBUTE_LIST_SUCCESS = '[Documentation] Retrieve Attribute List Success';
export const RETRIEVE_ATTRIBUTE_LIST_FAIL = '[Documentation] Retrieve Attribute List Fail';
export class RetrieveDatasetList implements Action {
readonly type = RETRIEVE_DATASET_LIST;
constructor(public payload: {} = null) { }
}
export class RetrieveDatasetListWip implements Action {