diff --git a/client/src/app/instance/doc/containers/doc.component.html b/client/src/app/instance/doc/containers/doc.component.html new file mode 100644 index 0000000000000000000000000000000000000000..d241add0af293f86f4585234bb9ca7898832aea4 --- /dev/null +++ b/client/src/app/instance/doc/containers/doc.component.html @@ -0,0 +1,205 @@ +<app-spinner *ngIf="attributeListIsLoading | async"></app-spinner> + +<div *ngIf="attributeListIsLoaded | async" 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>{{ getUrlServer() }}/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 separated 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="attributeListsIsLoading | async"> + <span class="fas fa-circle-notch fa-spin fa-3x"></span> + <span class="sr-only">Loading...</span> + </div> + <div *ngIf="attributeListsIsLoaded | async" class="row"> + <div *ngFor="let dataset of datasetList | async" class="col-auto mb-5"> + <h6>{{ dataset.label }} output list</h6> + <table id="table" class="attributes-table p-0"> + <tr> + <th>id</th> + <th>attribute</th> + </tr> + <tr *ngFor="let attribute of getAttributeList(dataset.name, 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>{{ getUrlServer() }}/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> + {{ getUrlServer() }}/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> + {{ getUrlServer() }}/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> diff --git a/client/src/app/instance/doc/containers/doc.component.ts b/client/src/app/instance/doc/containers/doc.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ba2de8f1722d03909ad9bcd0b79a77b988e215b --- /dev/null +++ b/client/src/app/instance/doc/containers/doc.component.ts @@ -0,0 +1,88 @@ +/** + * 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, OnInit } from '@angular/core'; + +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; + +import * as documentationActions from 'src/app/instance/store/actions/documentation.actions'; +import * as datasetActions from 'src/app/metamodel/actions/dataset.actions'; +import * as datasetFamilySelector from 'src/app/metamodel/selectors/dataset-family.selector'; +import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector'; +import {Attribute, Dataset, DatasetFamily, Survey} from 'src/app/metamodel/models'; +import { environment } from 'src/environments/environment'; +import * as authSelector from "../../../auth/auth.selector"; +import * as instanceSelector from "../../../metamodel/selectors/instance.selector"; +import * as surveySelector from "../../../metamodel/selectors/survey.selector"; +import * as attributeSelector from "../../../metamodel/selectors/attribute.selector"; + +@Component({ + selector: 'app-doc', + templateUrl: 'doc.component.html', + styleUrls: ['../doc.component.scss'] +}) +/** + * @class + * @classdesc Documentation container. + * + * @implements OnInit + */ +export class DocComponent implements OnInit { + public instanceSelected: Observable<string>; + public datasetSelected: Observable<string>; + public attributeListIsLoading: Observable<boolean>; + public attributeListIsLoaded: Observable<boolean>; + public attributeList: Observable<Attribute[]>; + // public isAuthenticated: Observable<boolean>; + // public datasetFamilyListIsLoading: Observable<boolean>; + // public datasetFamilyListIsLoaded: Observable<boolean>; + // public datasetFamilyList: Observable<DatasetFamily[]>; + // public surveyListIsLoading: Observable<boolean>; + // public surveyListIsLoaded: Observable<boolean>; + // public surveyList: Observable<Survey[]>; + // public datasetListIsLoading: Observable<boolean>; + // public datasetListIsLoaded: Observable<boolean>; + // public datasetList: Observable<Dataset[]>; + + constructor(private store: Store<{ }>) { + this.instanceSelected = store.select(instanceSelector.selectInstanceNameByRoute); + this.datasetSelected = store.select(datasetSelector.selectDatasetNameByRoute); + this.attributeListIsLoading = store.select(attributeSelector.selectAttributeListIsLoading); + this.attributeListIsLoaded = store.select(attributeSelector.selectAttributeListIsLoaded); + this.attributeList = store.select(attributeSelector.selectAllAttributes); + // this.isAuthenticated = store.select(authSelector.selectIsAuthenticated); + // this.datasetFamilyListIsLoading = store.select(datasetFamilySelector.selectDatasetFamilyListIsLoading); + // this.datasetFamilyListIsLoaded = store.select(datasetFamilySelector.selectDatasetFamilyListIsLoaded); + // this.datasetFamilyList = store.select(datasetFamilySelector.selectAllDatasetFamilies); + // this.surveyListIsLoading = store.select(surveySelector.selectSurveyListIsLoading); + // this.surveyListIsLoaded = store.select(surveySelector.selectSurveyListIsLoaded); + // this.surveyList = store.select(surveySelector.selectAllSurveys); + // this.datasetListIsLoading = store.select(datasetSelector.selectDatasetListIsLoading); + // this.datasetListIsLoaded = store.select(datasetSelector.selectDatasetListIsLoaded); + // this.datasetList = store.select(datasetSelector.selectAllDatasets); + } + + ngOnInit() { + this.store.dispatch(documentationActions.loadAttributeList()); + } + + /** + * Returns strict url address. + * + * @return string + */ + getUrlServer(): string { + if (!environment.apiUrl.startsWith('http')) { + const url = window.location; + return url.protocol + '//' + url.host + environment.apiUrl; + } + return environment.apiUrl; + } +} diff --git a/client/src/app/instance/store/actions/documentation.actions.ts b/client/src/app/instance/store/actions/documentation.actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..d2da6760840f913e1261cff8e014ec11048aa438 --- /dev/null +++ b/client/src/app/instance/store/actions/documentation.actions.ts @@ -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. + */ + +import { createAction } from '@ngrx/store'; + +export const loadAttributeList = createAction('[Documentation] Load Attribute List'); diff --git a/client/src/app/instance/store/effects/documentation.effects.ts b/client/src/app/instance/store/effects/documentation.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..b43a756ff76b9df2b76280d9a9bd7d0679314eb7 --- /dev/null +++ b/client/src/app/instance/store/effects/documentation.effects.ts @@ -0,0 +1,211 @@ +/** + * 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 { of } from 'rxjs'; +import { map, tap, mergeMap, catchError } from 'rxjs/operators'; +import { ToastrService } from 'ngx-toastr'; + +import { criterionToString, stringToCriterion } from '../models'; +import { SearchService } from '../services/search.service'; +import * as documentationActions from '../actions/documentation.actions'; +import * as searchActions from '../actions/search.actions'; +import * as attributeActions from 'src/app/metamodel/actions/attribute.actions'; +import * as attributeSelector from 'src/app/metamodel/selectors/attribute.selector'; +import * as criteriaFamilyActions from 'src/app/metamodel/actions/criteria-family.actions'; +import * as outputFamilyActions from 'src/app/metamodel/actions/output-family.actions'; +import * as outputCategoryActions from 'src/app/metamodel/actions/output-category.actions'; +import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector'; +import * as searchSelector from '../selectors/search.selector'; + +@Injectable() +export class DocumentationEffects { + loadAttributeList$ = createEffect(() => + this.actions$.pipe( + ofType(documentationActions.loadAttributeList), + concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)), + mergeMap(([action, datasetName]) => { + console.log('ok'); + return of(attributeActions.loadAttributeList()); + }) + ) + ); + + // initSearch$ = createEffect(() => + // this.actions$.pipe( + // ofType(searchActions.initSearch), + // concatLatestFrom(() => [ + // this.store.select(datasetSelector.selectDatasetNameByRoute), + // this.store.select(searchSelector.selectCurrentDataset), + // this.store.select(searchSelector.selectPristine), + // this.store.select(searchSelector.selectStepsByRoute) + // ]), + // mergeMap(([action, datasetName, currentDataset, pristine, steps]) => { + // // User has changed dataset: reload initial state + init search + // if (datasetName && currentDataset && datasetName !== currentDataset) { + // return of(searchActions.restartSearch()); + // } + // + // // User has selected a dataset or page is reloaded: load dataset metamodel + // if (datasetName && pristine) { + // let actions: Action[] = [ + // searchActions.changeCurrentDataset({ currentDataset: datasetName }), + // attributeActions.loadAttributeList(), + // criteriaFamilyActions.loadCriteriaFamilyList(), + // outputFamilyActions.loadOutputFamilyList(), + // outputCategoryActions.loadOutputCategoryList() + // ]; + // if (steps) { + // if(steps[0] === '1') { + // actions.push(searchActions.checkCriteria()); + // } + // if(steps[1] === '1') { + // actions.push(searchActions.checkOutput()); + // } + // if(steps[2] === '1') { + // actions.push(searchActions.checkResult()); + // } + // } + // return actions; + // } + // + // // User come back to the search module: reload initial state + // if(!datasetName && !pristine) { + // return of(searchActions.resetSearch()); + // } + // + // // User change step and it's the same search: No action + // return of({ type: '[No Action] Init Search' }); + // }) + // ) + // ); + // + // restartSearch$ = createEffect(() => + // this.actions$.pipe( + // ofType(searchActions.restartSearch), + // map(() => searchActions.initSearch()) + // ) + // ); + // + // loadDefaultFormParameters$ = createEffect(() => + // this.actions$.pipe( + // ofType(searchActions.loadDefaultFormParameters), + // concatLatestFrom(() => [ + // this.store.select(attributeSelector.selectAllAttributes), + // this.store.select(searchSelector.selectCriteriaListByRoute), + // this.store.select(searchSelector.selectOutputListByRoute) + // ]), + // mergeMap(([action, attributeList, criteriaList, outputList]) => { + // // Update criteria list + // let defaultCriteriaList = []; + // if (criteriaList) { + // // Build criteria list with the URL query parameters (c) + // defaultCriteriaList = criteriaList.split(';').map((c: string) => { + // const params = c.split('::'); + // const attribute = attributeList.find(a => a.id === parseInt(params[0], 10)); + // return stringToCriterion(attribute, params); + // }); + // } else { + // // Build default criteria list with the attribute list metamodel configuration + // defaultCriteriaList = attributeList + // .filter(attribute => attribute.id_criteria_family && attribute.search_type && attribute.min) + // .map(attribute => stringToCriterion(attribute)); + // } + // + // // Update output list + // let defaultOutputList = []; + // if (outputList) { + // // Build output list with the URL query parameters (a) + // defaultOutputList = outputList.split(';').map((o: string) => parseInt(o, 10)); + // } else { + // // Build default output list with the attribute list metamodel configuration + // defaultOutputList = attributeList + // .filter(attribute => attribute.selected && attribute.id_output_category) + // .map(attribute => attribute.id); + // } + // + // // Returns actions and mark the form as dirty + // return [ + // searchActions.updateCriteriaList({ criteriaList: defaultCriteriaList }), + // searchActions.updateOutputList({ outputList: defaultOutputList }), + // searchActions.markAsDirty() + // ]; + // }) + // ) + // ); + // + // retrieveDataLength$ = createEffect(() => + // this.actions$.pipe( + // ofType(searchActions.retrieveDataLength), + // concatLatestFrom(() => [ + // this.store.select(datasetSelector.selectDatasetNameByRoute), + // this.store.select(searchSelector.selectCriteriaList) + // ]), + // mergeMap(([action, datasetName, criteriaList]) => { + // let query = datasetName + '?a=count'; + // if (criteriaList.length > 0) { + // query += '&c=' + criteriaList.map(criterion => criterionToString(criterion)).join(';'); + // } + // + // return this.searchService.retrieveDataLength(query) + // .pipe( + // map((response: { nb: number }[]) => searchActions.retrieveDataLengthSuccess({ length: response[0].nb })), + // catchError(() => of(searchActions.retrieveDataLengthFail())) + // ) + // }) + // ) + // ); + // + // retrieveDataLengthFail$ = createEffect(() => + // this.actions$.pipe( + // ofType(searchActions.retrieveDataLengthFail), + // tap(() => this.toastr.error('Loading Failed', 'The search data length loading failed')) + // ), { dispatch: false} + // ); + // + // retrieveData$ = createEffect(() => + // this.actions$.pipe( + // ofType(searchActions.retrieveData), + // concatLatestFrom(() => [ + // this.store.select(datasetSelector.selectDatasetNameByRoute), + // this.store.select(searchSelector.selectCriteriaList), + // this.store.select(searchSelector.selectOutputList) + // ]), + // mergeMap(([action, datasetName, criteriaList, outputList]) => { + // let query = datasetName + '?a=' + outputList.join(';'); + // if (criteriaList.length > 0) { + // query += '&c=' + criteriaList.map(criterion => criterionToString(criterion)).join(';'); + // } + // query += '&p=' + action.pagination.nbItems + ':' + action.pagination.page; + // query += '&o=' + action.pagination.sortedCol + ':' + action.pagination.order; + // + // return this.searchService.retrieveData(query) + // .pipe( + // map((data: any[]) => searchActions.retrieveDataSuccess({ data })), + // catchError(() => of(searchActions.retrieveDataFail())) + // ) + // }) + // ) + // ); + // + // retrieveDataFail$ = createEffect(() => + // this.actions$.pipe( + // ofType(searchActions.retrieveDataFail), + // tap(() => this.toastr.error('Loading Failed', 'The search data loading failed')) + // ), { dispatch: false} + // ); + + constructor( + private actions$: Actions, + private store: Store<{ }> + ) {} +} diff --git a/client/src/app/instance/store/effects/index.ts b/client/src/app/instance/store/effects/index.ts index 296d89b24794a19ce38357ac7eab009ed9234257..6c5a834023827b54a609d049daa42baa9b829be3 100644 --- a/client/src/app/instance/store/effects/index.ts +++ b/client/src/app/instance/store/effects/index.ts @@ -1,9 +1,11 @@ import { MetamodelEffects } from './metamodel.effects'; import { SampEffects } from "./samp.effects"; import { SearchEffects } from "./search.effects"; +import { DocumentationEffects } from "./documentation.effects"; export const instanceEffects = [ MetamodelEffects, SampEffects, - SearchEffects + SearchEffects, + DocumentationEffects ];