diff --git a/CHANGELOG.md b/CHANGELOG.md index e1544830702feedc316da6018f23eaf253cdc61b..b995ca18bd41c6b08d11db107d522f81071abef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ 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-05 +### Added +- #90 => Dynamic documentation to explain how to export results via server urls + +### Fixed + +### Changed + +### Security + + ## [3.2.0] - 2020-04 ### Added - #69 => User can change the number of object displayable (10, 20, 50, 100) in result datatable diff --git a/VERSION b/VERSION index 06a445799fe7411076dfdc156c700d3b1e199deb..f30101c08059da605966d4e84d357519b8c77283 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1 \ No newline at end of file +3.3 \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 043950cdec8d1dd4a059d6a10ba0267fe9b8ad0b..c59f2ab0474fc9c6dbfb5c9bcd5d01763e0d64e7 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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 { } diff --git a/src/app/core/components/nav.component.html b/src/app/core/components/nav.component.html index f2f73abcdb21881506d645d480be5d6c0942f30e..1412b47e3c49964b7136e0ea542e6ee719f95f72 100644 --- a/src/app/core/components/nav.component.html +++ b/src/app/core/components/nav.component.html @@ -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"> diff --git a/src/app/documentation/containers/documentation.component.css b/src/app/documentation/containers/documentation.component.css new file mode 100644 index 0000000000000000000000000000000000000000..288c15cc9fa95895666f5b0e358aab583b8466fd --- /dev/null +++ b/src/app/documentation/containers/documentation.component.css @@ -0,0 +1,14 @@ +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; +} diff --git a/src/app/documentation/containers/documentation.component.html b/src/app/documentation/containers/documentation.component.html new file mode 100644 index 0000000000000000000000000000000000000000..2feed592bf8d7d18e2e6f6386c5a48b1d7c9cde3 --- /dev/null +++ b/src/app/documentation/containers/documentation.component.html @@ -0,0 +1,203 @@ +<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 diff --git a/src/app/documentation/containers/documentation.component.spec.ts b/src/app/documentation/containers/documentation.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a50dd865065651dfea5acc8086f453cb7725fdc --- /dev/null +++ b/src/app/documentation/containers/documentation.component.spec.ts @@ -0,0 +1,45 @@ +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); + }); +}); + diff --git a/src/app/documentation/containers/documentation.component.ts b/src/app/documentation/containers/documentation.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..14fd652facdc5dd3f137963335498e4a64c17349 --- /dev/null +++ b/src/app/documentation/containers/documentation.component.ts @@ -0,0 +1,44 @@ +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);; + } +} diff --git a/src/app/documentation/documentation.module.ts b/src/app/documentation/documentation.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..113382034ea9b6009368ca03b42f9a3ce4d4f56e --- /dev/null +++ b/src/app/documentation/documentation.module.ts @@ -0,0 +1,26 @@ +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 { } diff --git a/src/app/documentation/documentation.routing.ts b/src/app/documentation/documentation.routing.ts new file mode 100644 index 0000000000000000000000000000000000000000..1c157868b1e31836c836b1cd58e50ac23e8ecbfc --- /dev/null +++ b/src/app/documentation/documentation.routing.ts @@ -0,0 +1,18 @@ +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 +]; diff --git a/src/app/documentation/store/documentation.action.spec.ts b/src/app/documentation/store/documentation.action.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..11cb06849d205431d67cf1f8b0bbde71277f12d8 --- /dev/null +++ b/src/app/documentation/store/documentation.action.spec.ts @@ -0,0 +1,45 @@ +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); + }); +}); diff --git a/src/app/documentation/store/documentation.action.ts b/src/app/documentation/store/documentation.action.ts new file mode 100644 index 0000000000000000000000000000000000000000..7cd0fc627d83606f1d9f189551e424d0221e774d --- /dev/null +++ b/src/app/documentation/store/documentation.action.ts @@ -0,0 +1,62 @@ +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 { + readonly type = RETRIEVE_DATASET_LIST_WIP; + + constructor(public payload: {} = null) { } +} + +export class RetrieveDatasetListSuccess implements Action { + readonly type = RETRIEVE_DATASET_LIST_SUCCESS; + + constructor(public payload: Dataset[]) { } +} + +export class RetrieveDatasetListFail implements Action { + readonly type = RETRIEVE_DATASET_LIST_FAIL; + + constructor(public payload: {} = null) { } +} + +export class RetrieveAttributeList implements Action { + readonly type = RETRIEVE_ATTRIBUTE_LIST; + + constructor(public payload: string[]) { } +} + +export class RetrieveAttributeListSuccess implements Action { + readonly type = RETRIEVE_ATTRIBUTE_LIST_SUCCESS; + + constructor(public payload: Attribute[]) { } +} + +export class RetrieveAttributeListFail implements Action { + readonly type = RETRIEVE_ATTRIBUTE_LIST_FAIL; + + constructor(public payload: {} = null) { } +} + +export type Actions + = RetrieveDatasetList + | RetrieveDatasetListWip + | RetrieveDatasetListSuccess + | RetrieveDatasetListFail + | RetrieveAttributeList + | RetrieveAttributeListSuccess + | RetrieveAttributeListFail; diff --git a/src/app/documentation/store/documentation.effects.ts b/src/app/documentation/store/documentation.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..01329f35553794c10d0f475854168ab1bd3ac990 --- /dev/null +++ b/src/app/documentation/store/documentation.effects.ts @@ -0,0 +1,106 @@ +import { Injectable } from '@angular/core'; + +import { ToastrService } from 'ngx-toastr'; +import { Effect, Actions, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { of, Observable, forkJoin } from 'rxjs'; +import { map, tap, switchMap, withLatestFrom, catchError } from 'rxjs/operators'; + +import * as fromRouter from '@ngrx/router-store'; +import * as fromDocumentation from './documentation.reducer'; +import * as documentationActions from './documentation.action'; +import * as utils from '../../shared/utils'; +import { DocumentationService } from './documentation.service'; +import { Dataset, Attribute } from 'src/app/metamodel/model'; + +@Injectable() +export class DocumentationEffects { + constructor( + private actions$: Actions, + private documentationService: DocumentationService, + private toastr: ToastrService, + private store$: Store<{ + router: fromRouter.RouterReducerState<utils.RouterStateUrl>, + documentation: fromDocumentation.State + }> + ) { } + + @Effect() + retrieveDatasetListAction$ = this.actions$.pipe( + ofType(documentationActions.RETRIEVE_DATASET_LIST), + withLatestFrom(this.store$), + switchMap(([action, state]) => { + if (state.documentation.datasetListIsLoaded) { + return of({ type: '[No Action]Â [Documentation] Dataset list is already loaded' }); + } else { + return of(new documentationActions.RetrieveDatasetListWip()); + } + }) + ); + + @Effect() + retrieveDatasetListWipAction$ = this.actions$.pipe( + ofType(documentationActions.RETRIEVE_DATASET_LIST_WIP), + switchMap(_ => + this.documentationService.retrieveDatasetList().pipe( + map((datasetList: Dataset[]) => + new documentationActions.RetrieveDatasetListSuccess(datasetList)), + catchError(() => of(new documentationActions.RetrieveDatasetListFail())) + ) + ) + ); + + @Effect({ dispatch: false }) + retrieveDatasetListFailedAction$ = this.actions$.pipe( + ofType(documentationActions.RETRIEVE_DATASET_LIST_FAIL), + tap(_ => this.toastr.error('Loading Failed!', 'Dataset list loading failed')) + ); + + @Effect() + retrieveDatasetListSuccessAction$ = this.actions$.pipe( + ofType(documentationActions.RETRIEVE_DATASET_LIST_SUCCESS), + map(action => { + const retrieveDatasetListSuccessAction = action as documentationActions.RetrieveDatasetListSuccess; + const datasetList = retrieveDatasetListSuccessAction.payload; + const dnames: string[] = []; + datasetList.forEach(dataset => { + dnames.push(dataset.name); + }); + return new documentationActions.RetrieveAttributeList(dnames); + }) + ); + + @Effect() + retrieveAttributeListAction$ = this.actions$.pipe( + ofType(documentationActions.RETRIEVE_ATTRIBUTE_LIST), + switchMap((action) => { + const retrieveAttributetListAction = action as documentationActions.RetrieveAttributeList; + const dnames = retrieveAttributetListAction.payload; + let requests: Observable<any>[] = []; + dnames.forEach(dname => { + requests.push(this.documentationService.retrieveAttributetList(dname)); + }); + return forkJoin(requests); + }), + switchMap((data) => { + let attributes: Attribute[] = []; + data.forEach(d => { + attributes = [...attributes, ...d] + }); + return of(new documentationActions.RetrieveAttributeListSuccess(attributes)); + }), + catchError(_ => { + return of(new documentationActions.RetrieveAttributeListFail()); + }) + ); + + @Effect({ dispatch: false }) + retrieveAttributeListFailedAction$ = this.actions$.pipe( + ofType(documentationActions.RETRIEVE_DATASET_LIST_FAIL), + tap((action) => { + const retrieveAttributeListFailedAction = action as documentationActions.RetrieveAttributeListFail; + const dname = retrieveAttributeListFailedAction.payload; + this.toastr.error('Loading Failed!', 'Attribute list for dataset ' + dname + ' loading failed'); + }) + ); +} diff --git a/src/app/documentation/store/documentation.reducer.spec.ts b/src/app/documentation/store/documentation.reducer.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..d883352cf7bcd3d1c17fcefec471b7d8936ae88c --- /dev/null +++ b/src/app/documentation/store/documentation.reducer.spec.ts @@ -0,0 +1,143 @@ +import * as fromDocumentation from './documentation.reducer'; +import * as documentationActions from './documentation.action'; +import { Dataset, Attribute } from 'src/app/metamodel/model'; +import { DATASET_LIST, DATASET, ATTRIBUTE_LIST } from 'src/settings/test-data'; + +describe('[Documentation] Reducer', () => { + it('should return init state', () => { + const { initialState } = fromDocumentation; + const action = {} as documentationActions.Actions; + const state = fromDocumentation.reducer(undefined, action); + + expect(state).toBe(initialState); + }); + + it('should set datasetListIsLoading to true', () => { + const { initialState } = fromDocumentation; + const action = new documentationActions.RetrieveDatasetListWip(); + const state = fromDocumentation.reducer(initialState, action); + + expect(state.datasetListIsLoading).toBeTruthy(); + expect(state.datasetListIsLoaded).toBeFalsy(); + expect(state.datasetList).toBeNull(); + expect(state.attributeListIsLoading).toBeFalsy(); + expect(state.attributeListIsLoaded).toBeFalsy(); + expect(state.attributeList).toBeNull(); + expect(state).not.toEqual(initialState); + }); + + it('should set datasetList, datasetListIsLoaded to true and datasetListIsLoading to false', () => { + const datasetList: Dataset[] = DATASET_LIST; + const initialState = { ...fromDocumentation.initialState, datasetListIsLoading: true }; + const action = new documentationActions.RetrieveDatasetListSuccess(datasetList); + const state = fromDocumentation.reducer(initialState, action); + + expect(state.datasetListIsLoading).toBeFalsy(); + expect(state.datasetListIsLoaded).toBeTruthy(); + expect(state.datasetList).toEqual(datasetList); + expect(state.attributeListIsLoading).toBeFalsy(); + expect(state.attributeListIsLoaded).toBeFalsy(); + expect(state.attributeList).toBeNull(); + expect(state).not.toEqual(initialState); + }); + + it('should set datasetListIsLoading to false', () => { + const initialState = { ...fromDocumentation.initialState, datasetListIsLoading: true }; + const action = new documentationActions.RetrieveDatasetListFail(); + const state = fromDocumentation.reducer(initialState, action); + + expect(state.datasetListIsLoading).toBeFalsy(); + expect(state.datasetListIsLoaded).toBeFalsy(); + expect(state.datasetList).toBeNull(); + expect(state.attributeListIsLoading).toBeFalsy(); + expect(state.attributeListIsLoaded).toBeFalsy(); + expect(state.attributeList).toBeNull(); + expect(state).not.toEqual(initialState); + }); + + it('should set attributeListIsLoading to true', () => { + const dataset: Dataset = DATASET; + const { initialState } = fromDocumentation; + const action = new documentationActions.RetrieveAttributeList([dataset.name]); + const state = fromDocumentation.reducer(initialState, action); + + expect(state.datasetListIsLoading).toBeFalsy(); + expect(state.datasetListIsLoaded).toBeFalsy(); + expect(state.datasetList).toBeNull(); + expect(state.attributeListIsLoading).toBeTruthy(); + expect(state.attributeListIsLoaded).toBeFalsy(); + expect(state.attributeList).toBeNull(); + expect(state).not.toEqual(initialState); + }); + + it('should set datasetList, attributeListIsLoaded to true and attributeListIsLoading to false', () => { + const attributeList: Attribute[] = ATTRIBUTE_LIST; + const initialState = { ...fromDocumentation.initialState, attributeListIsLoading: true }; + const action = new documentationActions.RetrieveAttributeListSuccess(attributeList); + const state = fromDocumentation.reducer(initialState, action); + + expect(state.datasetListIsLoading).toBeFalsy(); + expect(state.datasetListIsLoaded).toBeFalsy(); + expect(state.datasetList).toBeNull(); + expect(state.attributeListIsLoading).toBeFalsy(); + expect(state.attributeListIsLoaded).toBeTruthy(); + expect(state.attributeList).toEqual(attributeList); + expect(state).not.toEqual(initialState); + }); + + it('should set attributeListIsLoading to false', () => { + const initialState = { ...fromDocumentation.initialState, attributeListIsLoading: true }; + const action = new documentationActions.RetrieveAttributeListFail(); + const state = fromDocumentation.reducer(initialState, action); + + expect(state.datasetListIsLoading).toBeFalsy(); + expect(state.datasetListIsLoaded).toBeFalsy(); + expect(state.datasetList).toBeNull(); + expect(state.attributeListIsLoading).toBeFalsy(); + expect(state.attributeListIsLoaded).toBeFalsy(); + expect(state.attributeList).toBeNull(); + expect(state).not.toEqual(initialState); + }); + + it('should get datasetListIsLoading', () => { + const action = {} as documentationActions.Actions; + const state = fromDocumentation.reducer(undefined, action); + + expect(fromDocumentation.getDatasetListIsLoading(state)).toBeFalsy(); + }); + + it('should get datasetListIsLoaded', () => { + const action = {} as documentationActions.Actions; + const state = fromDocumentation.reducer(undefined, action); + + expect(fromDocumentation.getDatasetListIsLoaded(state)).toBeFalsy(); + }); + + it('should get datasetList', () => { + const action = {} as documentationActions.Actions; + const state = fromDocumentation.reducer(undefined, action); + + expect(fromDocumentation.getDatasetList(state)).toBeNull(); + }); + + it('should get attributeListIsLoading', () => { + const action = {} as documentationActions.Actions; + const state = fromDocumentation.reducer(undefined, action); + + expect(fromDocumentation.getAttributeListIsLoading(state)).toBeFalsy(); + }); + + it('should get attributeListIsLoaded', () => { + const action = {} as documentationActions.Actions; + const state = fromDocumentation.reducer(undefined, action); + + expect(fromDocumentation.getAttributeListIsLoaded(state)).toBeFalsy(); + }); + + it('should get attributeList', () => { + const action = {} as documentationActions.Actions; + const state = fromDocumentation.reducer(undefined, action); + + expect(fromDocumentation.getAttributeList(state)).toBeNull(); + }); +}); diff --git a/src/app/documentation/store/documentation.reducer.ts b/src/app/documentation/store/documentation.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..ff5b20a05f1103c2c9e656bfb054f9a1a2f93eeb --- /dev/null +++ b/src/app/documentation/store/documentation.reducer.ts @@ -0,0 +1,77 @@ +import * as actions from './documentation.action'; +import { Dataset, Attribute } from 'src/app/metamodel/model'; + +export interface State { + datasetListIsLoading: boolean; + datasetListIsLoaded: boolean; + datasetList: Dataset[]; + attributeListIsLoading: boolean; + attributeListIsLoaded: boolean; + attributeList: Attribute[]; +} + +export const initialState: State = { + datasetListIsLoading: false, + datasetListIsLoaded: false, + datasetList: null, + attributeListIsLoading: false, + attributeListIsLoaded: false, + attributeList: null +}; + +export function reducer(state: State = initialState, action: actions.Actions): State { + switch (action.type) { + case actions.RETRIEVE_DATASET_LIST_WIP: + return { + ...state, + datasetListIsLoading: true + }; + + case actions.RETRIEVE_DATASET_LIST_SUCCESS: + const datasetList = action.payload as Dataset[]; + return { + ...state, + datasetList, + datasetListIsLoading: false, + datasetListIsLoaded: true + }; + + case actions.RETRIEVE_DATASET_LIST_FAIL: + return { + ...state, + datasetListIsLoading: false + }; + + case actions.RETRIEVE_ATTRIBUTE_LIST: + return { + ...state, + attributeListIsLoading: true + }; + + case actions.RETRIEVE_ATTRIBUTE_LIST_SUCCESS: + const attributeList = action.payload as Attribute[]; + + return { + ...state, + attributeList, + attributeListIsLoading: false, + attributeListIsLoaded: true + }; + + case actions.RETRIEVE_ATTRIBUTE_LIST_FAIL: + return { + ...state, + attributeListIsLoading: false + }; + + default: + return state; + } +} + +export const getDatasetListIsLoading = (state: State) => state.datasetListIsLoading; +export const getDatasetListIsLoaded = (state: State) => state.datasetListIsLoaded; +export const getDatasetList = (state: State) => state.datasetList; +export const getAttributeListIsLoading = (state: State) => state.attributeListIsLoading; +export const getAttributeListIsLoaded = (state: State) => state.attributeListIsLoaded; +export const getAttributeList = (state: State) => state.attributeList; diff --git a/src/app/documentation/store/documentation.selector.spec.ts b/src/app/documentation/store/documentation.selector.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..b1fdb8f0bb96c68f765e5b9f8ddccf49dffff7af --- /dev/null +++ b/src/app/documentation/store/documentation.selector.spec.ts @@ -0,0 +1,39 @@ +import * as documentationSelector from './documentation.selector'; +import * as fromDocumentation from './documentation.reducer'; + +describe('[Documentation] Selector', () => { + it('should get datasetIsLoading', () => { + const state = { documentation: { ...fromDocumentation.initialState }}; + expect(documentationSelector.getDatasetListIsLoading(state)).toBeFalsy(); + }); + + it('should get datasetListIsLoaded', () => { + const state = { documentation: { ...fromDocumentation.initialState }}; + expect(documentationSelector.getDatasetListIsLoaded(state)).toBeFalsy(); + }); + + it('should get datasetListIsLoaded', () => { + const state = { documentation: { ...fromDocumentation.initialState }}; + expect(documentationSelector.getDatasetListIsLoaded(state)).toBeFalsy(); + }); + + it('should get datasetList', () => { + const state = { documentation: { ...fromDocumentation.initialState }}; + expect(documentationSelector.getDatasetList(state)).toBeNull(); + }); + + it('should get attributeListIsLoading', () => { + const state = { documentation: { ...fromDocumentation.initialState }}; + expect(documentationSelector.getAttributeListIsLoading(state)).toBeFalsy(); + }); + + it('should get attributeListIsLoaded', () => { + const state = { documentation: { ...fromDocumentation.initialState }}; + expect(documentationSelector.getAttributeListIsLoaded(state)).toBeFalsy(); + }); + + it('should get attributeList', () => { + const state = { documentation: { ...fromDocumentation.initialState }}; + expect(documentationSelector.getAttributeList(state)).toBeNull(); + }); +}); diff --git a/src/app/documentation/store/documentation.selector.ts b/src/app/documentation/store/documentation.selector.ts new file mode 100644 index 0000000000000000000000000000000000000000..b904e9cedd7f0ec9ca0d72c65548745d93108e7d --- /dev/null +++ b/src/app/documentation/store/documentation.selector.ts @@ -0,0 +1,35 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; + +import * as documentation from './documentation.reducer'; + +export const getDocumentationState = createFeatureSelector<documentation.State>('documentation'); + +export const getDatasetListIsLoading = createSelector( + getDocumentationState, + documentation.getDatasetListIsLoading +); + +export const getDatasetListIsLoaded = createSelector( + getDocumentationState, + documentation.getDatasetListIsLoaded +); + +export const getDatasetList = createSelector( + getDocumentationState, + documentation.getDatasetList +); + +export const getAttributeListIsLoading = createSelector( + getDocumentationState, + documentation.getAttributeListIsLoading +); + +export const getAttributeListIsLoaded = createSelector( + getDocumentationState, + documentation.getAttributeListIsLoaded +); + +export const getAttributeList = createSelector( + getDocumentationState, + documentation.getAttributeList +); diff --git a/src/app/documentation/store/documentation.service.ts b/src/app/documentation/store/documentation.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..eb19869543058596870ebb4cfe35b8b7f631f359 --- /dev/null +++ b/src/app/documentation/store/documentation.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { environment } from '../../../environments/environment'; +import { Dataset, Attribute } from 'src/app/metamodel/model'; +import { Observable } from 'rxjs'; + +@Injectable() +export class DocumentationService { + API_PATH: string = environment.apiUrl; + instanceName: string = environment.instanceName; + + constructor(private http: HttpClient) { } + + retrieveDatasetList(): Observable<Dataset[]> { + return this.http.get<Dataset[]>(this.API_PATH + '/instance/' + this.instanceName + '/dataset'); + } + + retrieveAttributetList(dname: string): Observable<Attribute[]> { + return this.http.get<Attribute[]>(this.API_PATH + '/dataset/' + dname + '/attribute'); + } +} diff --git a/src/app/metamodel/model/attribute.model.ts b/src/app/metamodel/model/attribute.model.ts index 4ee8b3431d71b05d1845284cfcf735ff057e1b07..af702198f6db2669cd608c7ee57ecd36f969d150 100644 --- a/src/app/metamodel/model/attribute.model.ts +++ b/src/app/metamodel/model/attribute.model.ts @@ -35,4 +35,5 @@ export interface Attribute { vo_size: number; id_criteria_family: number; id_output_category: number; + } diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 4985cdaf40e6733786320a582ffec9c80010ab11..24bc1992be1040202cabbfbdd5bf65e3ce15fc4e 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -1,5 +1,5 @@ export const VERSIONS = { - anisClient: '3.2', + anisClient: '3.3', anisServer: '3.2', anisAuth: '3.2' }; diff --git a/src/settings/test-data/attribute-list.ts b/src/settings/test-data/attribute-list.ts index d0bdfbf788ac36ba6cb49ce112bc101a903eee64..a3fe9362f93f2a6c680518db8ff9142e20479014 100644 --- a/src/settings/test-data/attribute-list.ts +++ b/src/settings/test-data/attribute-list.ts @@ -4,7 +4,7 @@ export const ATTRIBUTE_LIST: Attribute[] = [ { id: 1, name: 'name_one', - table_name: 'table_one', + table_name: 'table_1', label: 'label_one', form_label: 'form_label_one', description: 'description_one', @@ -43,7 +43,7 @@ export const ATTRIBUTE_LIST: Attribute[] = [ { id: 2, name: 'name_two', - table_name: 'table_two', + table_name: 'table_2', label: 'label_two', form_label: 'form_label_two', description: 'description_two', diff --git a/src/settings/test-data/dataset.ts b/src/settings/test-data/dataset.ts index c9d8eda11fb460f8ce697f673a89137aaaf31424..f4302a5060f07ae9c926f9a740aeebaf0a4ea387 100644 --- a/src/settings/test-data/dataset.ts +++ b/src/settings/test-data/dataset.ts @@ -2,7 +2,7 @@ import { Dataset } from 'src/app/metamodel/model'; export const DATASET: Dataset = { name: 'cat_1', - table_ref: '', + table_ref: 'table_1', label: 'Cat 1', description: 'Description of cat 1', display: 10,