Commit 8578a328 authored by François Agneray's avatar François Agneray

Merge branch 'develop' into 'master'

3.1

See merge request !81
parents 387ceb00 d80c4370
......@@ -14,7 +14,7 @@ variables:
COVERAGE_IMAGE: portus.lam.fr/anis/anis-client-coverage
install:
image: node:11
image: node:13-slim
stage: install
script:
- yarn global add @angular/cli
......@@ -89,9 +89,9 @@ build:
stage: build
script:
- docker login -u fagneray -p $PORTUS_TOKEN portus.lam.fr
- docker pull $CONTAINER_IMAGE:latest-dev || true
- docker build --cache-from $CONTAINER_IMAGE:latest-dev -t $CONTAINER_IMAGE:latest-dev .
- docker push $CONTAINER_IMAGE:latest-dev
- docker pull $CONTAINER_IMAGE:latest || true
- docker build --cache-from $CONTAINER_IMAGE:latest -t $CONTAINER_IMAGE:latest .
- docker push $CONTAINER_IMAGE:latest
cache:
paths:
- dist
......
UID := $(id -u)
GID := $(id -g)
NAME_APP=anis-client
# UID := 1000
# GID := 1000
......@@ -9,13 +10,16 @@ list:
@echo "Useful targets:"
@echo ""
@echo " install > install node modules dependancies (node_modules)"
@echo " start > run a dev server for anis client application (in memory)"
@echo " stop > stop the dev server for anis client application"
@echo " restart > restart the dev server for anis client (container)"
@echo " test > run anis client tests"
@echo " ng-build > generate the angular dist application (html, css, js)"
@echo " log > display anis client container logs"
@echo " shell > shell into anis client container"
@echo " start > run a dev server for $(NAME_APP) application (in memory)"
@echo " stop > stop the dev server for $(NAME_APP) application"
@echo " restart > restart the dev server for $(NAME_APP) (container)"
@echo " status > display $(NAME_APP) container status"
@echo " test > run $(NAME_APP) tests"
@echo " test-watch > run $(NAME_APP) tests on every file change"
@echo " report > open the code coverage report in a browser (only available for Linux)"
@echo " dist > generate the angular dist application (html, css, js)"
@echo " logs > display $(NAME_APP) container logs"
@echo " shell > shell into $(NAME_APP) container"
@echo ""
install:
......@@ -25,7 +29,7 @@ install:
start:
@docker build -t anis-node conf-dev && docker run --init -it --rm --user $(UID):$(GID) \
--name anis-client \
--name $(NAME_APP) \
-p 4200:4200 \
-p 9876:9876 \
-v $(CURDIR):/project -d \
......@@ -34,20 +38,29 @@ start:
restart: stop start
stop:
@docker stop anis-client
@docker stop $(NAME_APP)
restart: stop start
status:
@docker ps -f name=$(NAME_APP)
test:
@docker exec -ti anis-client ng test --no-watch
@docker exec -ti $(NAME_APP) ng test --no-watch
test-watch:
@docker exec -ti $(NAME_APP) ng test
report:
xdg-open var/coverage/index.html
ng-build:
dist:
@docker build -t anis-node conf-dev && docker run --init -it --rm --user $(UID):$(GID) \
-v $(CURDIR):/project \
-w /project anis-node ng build --prod
log:
@docker logs -f -t anis-client
logs:
@docker logs -f -t $(NAME_APP)
shell:
@docker exec -ti anis-client bash
@docker exec -ti $(NAME_APP) bash
3.0.3
\ No newline at end of file
3.1
\ No newline at end of file
......@@ -81,7 +81,7 @@
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"codeCoverageExclude": ["node_modules/**", ""],
"codeCoverageExclude": ["node_modules/**", "src/settings/test-data/**"],
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
......
FROM node:12-slim
FROM node:13-slim
ENV DEBIAN_FRONTEND=noninteractive
......
<nav class="navbar navbar-light bg-light navbar-expand-md fixed-top border-bottom">
<!-- Logo -->
<a href="/" class="navbar-brand">
<img src="../assets/cesam_anis80.png" alt="CeSAM logo" />
<img src="assets/cesam_anis80.png" alt="CeSAM logo" />
</a>
<!-- Right Navigation -->
......
......@@ -11,32 +11,32 @@
<small>&copy; ANIS 2014 - {{ year }}</small>
</div>
<div class="row justify-content-center mb-4">
<small>Currently based on anis-client v{{ anisClientVersion }} and anis-server v{{ anisServerVersion }}. Code licensed CeCILL.</small>
<small>Currently based on anis-client v{{ anisClientVersion }}. Code licensed CeCILL.</small>
</div>
<div class="row justify-content-around">
<div class="col text-center">
<a href="http://cesam.lam.fr" title="Centre de données Astrophysique de Marseille">
<img class="img-fluid" src="../../../assets/logo_cesam_s.png" alt="CeSAM" />
<img class="img-fluid" src="assets/logo_cesam_s.png" alt="CeSAM" />
</a>
</div>
<div class="col text-center">
<a href="http://lam.oamp.fr" title="Laboratoire d'Astrophysique de Marseille">
<img class="img-fluid" src="../../../assets/logo_lam_s.png" alt="LAM" />
<img class="img-fluid" src="assets/logo_lam_s.png" alt="LAM" />
</a>
</div>
<div class="col text-center">
<a href="http://www.univ-amu.fr" title="Aix*Marseille Université">
<img class="img-fluid" src="../../../assets/logo_amu_s.png" alt="AMU" />
<img class="img-fluid" src="assets/logo_amu_s.png" alt="AMU" />
</a>
</div>
<div class="col text-center">
<a href="http://www.insu.cnrs.fr" title="Institut National des Sciences de l'Univers">
<img class="img-fluid" src="../../../assets/logo_insu_s.png" alt="INSU" />
<img class="img-fluid" src="assets/logo_insu_s.png" alt="INSU" />
</a>
</div>
<div class="col text-center">
<a href="http://anis.lam.fr" title="AstroNomical Information System">
<img class="img-fluid" src="../../../assets/cesam_anis40.png" alt="INSU" />
<img class="img-fluid" src="assets/cesam_anis40.png" alt="INSU" />
</a>
</div>
</div>
......
......@@ -16,7 +16,6 @@ import { VERSIONS } from '../../../settings/settings';
encapsulation: ViewEncapsulation.None
})
export class AppComponent implements OnInit {
anisServerVersion: string = VERSIONS.anisServer;
anisClientVersion: string = VERSIONS.anisClient;
year = (new Date()).getFullYear();
isAuthenticated: Observable<boolean>;
......
......@@ -11,6 +11,6 @@ export class DetailService {
retrieveObject(datasetName: string, attributeCriterionId: number, objectSelected: string, attributesOutputList: number[]) {
const query = datasetName + '?c=' + attributeCriterionId + '::eq::' + objectSelected + '&a=' + attributesOutputList.join(';');
return this.http.get<any[]>(this.API_PATH + '/data/' + query);
return this.http.get<any[]>(this.API_PATH + query);
}
}
......@@ -5,10 +5,17 @@
placeholder="Enter email" required autofocus>
<small class="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" class="form-control" name="password" [(ngModel)]="model.password"
placeholder="Password" required>
<label for="password">Password</label>
<div class="input-group mb-3">
<input [type]="showPassword ? 'text' : 'password'" id="password" class="form-control"
placeholder="Password" name="password" [(ngModel)]="model.password" aria-label="Password" required>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="button-addon2"
(click)="showPassword = !showPassword">
<div *ngIf="!showPassword"><span class="fas fa-fw fa-eye-slash"></span></div>
<div *ngIf="showPassword"><span class="fas fa-fw fa-eye"></span></div>
</button>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success" [disabled]="!f.form.valid || f.form.pristine">Sign In</button>
......
......@@ -8,6 +8,7 @@ import { Login } from '../store/model';
})
export class FormLoginComponent {
model: Login = new Login();
showPassword = false;
@Output() submitted: EventEmitter<Login> = new EventEmitter();
emitSubmit(login: Login) {
......
......@@ -5,15 +5,25 @@
placeholder="Enter email" required>
<small class="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
<div class="form-group">
<label for="confirm_email">Email confirmation</label>
<input type="email" id="confirm_email" class="form-control" name="confirm_email" placeholder="Enter email"
required>
</div>
<div class="form-group">
<label for="register_password">Password</label>
<input type="password" id="register_password" class="form-control" name="password" [(ngModel)]="model.password"
placeholder="Password" required>
<label for="register_password">Password</label>
<div class="input-group mb-3">
<input [type]="showPassword ? 'text' : 'password'" id="register_password" class="form-control"
placeholder="Password" name="password" [(ngModel)]="model.password" aria-label="Password" required>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="button-addon2"
(click)="showPassword = !showPassword">
<div *ngIf="!showPassword"><span class="fas fa-fw fa-eye-slash"></span></div>
<div *ngIf="showPassword"><span class="fas fa-fw fa-eye"></span></div>
</button>
</div>
</div>
<button type="submit" class="btn btn-success" [disabled]="!f.form.valid || f.form.pristine">Register</button>
</form>
\ No newline at end of file
......@@ -8,6 +8,7 @@ import { Login } from '../store/model';
})
export class FormRegisterComponent {
model: Login = new Login();
showPassword = false;
@Output() submitted: EventEmitter<Login> = new EventEmitter();
emitSubmit(login: Login) {
......
<form name="form" (ngSubmit)="f.form.valid && submitted()" #f="ngForm" novalidate>
<div class="form-group">
<label for="password">Password</label>
<input type="password"
id="password"
class="form-control"
name="password"
[(ngModel)]="model.password"
placeholder="Enter your password"
required>
<div class="container">
<div class="row justify-content-center">
<form name="form" (ngSubmit)="f.form.valid && submitted()" #f="ngForm" class="col-4" novalidate>
<label for="password">Password</label>
<div class="input-group mb-3">
<input [type]="showPassword ? 'text' : 'password'" id="password" class="form-control"
placeholder="Enter your password" name="password" [(ngModel)]="model.password" aria-label="Password" required>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" (click)="showPassword = !showPassword">
<div *ngIf="!showPassword"><span class="fas fa-fw fa-eye-slash"></span></div>
<div *ngIf="showPassword"><span class="fas fa-fw fa-eye"></span></div>
</button>
</div>
</div>
<label for="new_password">New password</label>
<div class="input-group mb-3">
<input [type]="showNewPassword ? 'text' : 'password'" id="new_password" class="form-control"
placeholder="Enter new password" name="new_password" [(ngModel)]="model.newPassword" aria-label="New Password" required>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" (click)="showNewPassword = !showNewPassword">
<div *ngIf="!showNewPassword"><span class="fas fa-fw fa-eye-slash"></span></div>
<div *ngIf="showNewPassword"><span class="fas fa-fw fa-eye"></span></div>
</button>
</div>
</div>
<div class="form-group text-center">
<button type="submit" class="btn btn-success" [disabled]="!f.form.valid || f.form.pristine">
Change password
</button>
</div>
</form>
</div>
<div class="form-group">
<label for="new_password">New password</label>
<input type="password"
id="new_password"
class="form-control"
name="new_password"
[(ngModel)]="model.newPassword"
placeholder="Enter new password"
required>
</div>
<div class="form-group">
<label for="confirm_password">Confirm assword</label>
<input type="password"
id="confirm_password"
class="form-control"
name="confirm_password"
[(ngModel)]="model.confirmPassword"
placeholder="Confirm new password"
required>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success" [disabled]="!f.form.valid || f.form.pristine">
Change password
</button>
</div>
</form>
\ No newline at end of file
</div>
\ No newline at end of file
......@@ -11,7 +11,9 @@ import * as fromLogin from '../store/login.reducer';
styleUrls: ['login.component.css']
})
export class ChangePasswordComponent {
model = { password: '', newPassword: '', confirmPassword: '' };
model = { password: '', newPassword: '' };
showPassword = false;
showNewPassword = false;
constructor(private store: Store<{ login: fromLogin.State }>) { }
......
.form-border {
border: 1px solid #dee2e6;
border-top: none;
border-bottom-left-radius: .25rem;
border-bottom-right-radius: .25rem;
}
\ No newline at end of file
<div class="row align-items-center">
<div class="col-12 mx-auto col-md-6 order-md-2 text-center">
<tabset [justified]="true">
<tab heading="Forgot password">
<div class="container text-left py-2">
<app-form-forgot-password (submitted)="forgotPassword($event)"></app-form-forgot-password>
<div class="container">
<div class="row align-items-center">
<div class="col-12 mx-auto col-md-6 order-md-2 text-center">
<tabset [justified]="true">
<tab heading="Forgot password">
<div class="container text-left py-2 form-border">
<app-form-forgot-password (submitted)="forgotPassword($event)"></app-form-forgot-password>
<div class="text-right">
<small>Already have login and password? <a routerLink="/login">Sign in</a></small>
<div class="text-right">
<small>Already have login and password? <a routerLink="/login">Sign in</a></small>
</div>
</div>
</div>
</tab>
</tabset>
</div>
<div class="col-md-6 order-md-1 text-justify text-md-left pr-md-5 align-self-start">
<app-account-benefits></app-account-benefits>
</tab>
</tabset>
</div>
<div class="col-md-6 order-md-1 text-justify text-md-left pr-md-5 align-self-start">
<app-account-benefits></app-account-benefits>
</div>
</div>
</div>
\ No newline at end of file
......@@ -8,7 +8,7 @@ import * as fromLogin from '../store/login.reducer';
@Component({
selector: 'app-forgot-password',
templateUrl: 'forgot-password.component.html',
styleUrls: ['login.component.css']
styleUrls: ['forgot-password.component.css']
})
export class ForgotPasswordComponent implements OnInit {
constructor(private store: Store<{ login: fromLogin.State }>) { }
......
<div class="row align-items-center">
<div class="col-12 mx-auto col-md-6 order-md-2 text-center">
<tabset [justified]="true" class="tabset">
<tab heading="Login">
<div class="container text-left py-2">
<app-form-login (submitted)="login($event)"></app-form-login>
<div class="container">
<div class="row align-items-center">
<div class="col-12 mx-auto col-md-6 order-md-2 text-center">
<tabset [justified]="true" class="tabset">
<tab heading="Login">
<div class="container text-left py-2">
<app-form-login (submitted)="login($event)"></app-form-login>
<div class="text-right">
<small><a routerLink="/forgot-password">Forgot your password?</a></small>
<div class="text-right">
<small><a routerLink="/forgot-password">Forgot your password?</a></small>
</div>
</div>
</div>
</tab>
<tab heading="Register">
<div class="container text-left py-2">
<app-form-register (submitted)="register($event)"></app-form-register>
</div>
</tab>
</tabset>
</div>
<div class="col-md-6 order-md-1 text-justify text-md-left pr-md-5 align-self-start">
<app-account-benefits></app-account-benefits>
</tab>
<tab heading="Register">
<div class="container text-left py-2">
<app-form-register (submitted)="register($event)"></app-form-register>
</div>
</tab>
</tabset>
</div>
<div class="col-md-6 order-md-1 text-justify text-md-left pr-md-5 align-self-start">
<app-account-benefits></app-account-benefits>
</div>
</div>
</div>
\ No newline at end of file
......@@ -9,14 +9,12 @@ import { environment } from '../../../environments/environment';
@Injectable()
export class LoginService {
private API_PATH: string = environment.apiUrl + '/';
private API_PATH: string = environment.authUrl;
constructor(private http: HttpClient) { }
login(login: Login): Observable<LoginToken> {
return this.http.post<{data: LoginToken}>(this.API_PATH + 'login', login).pipe(
map(res => res.data)
);
return this.http.post<LoginToken>(this.API_PATH + '/login', login);
}
resetConnectionToLocalstorage() {
......@@ -44,14 +42,14 @@ export class LoginService {
}
register(login: Login) {
return this.http.post(this.API_PATH + 'register', login);
return this.http.post(this.API_PATH + '/register', login);
}
forgotPassword(email: string) {
return this.http.post(this.API_PATH + 'new-password', email);
return this.http.post(this.API_PATH + '/new-password', email);
}
changePassword(email: string, changePassword: ChangePassword) {
return this.http.post(this.API_PATH + 'change-password', {...changePassword, email});
return this.http.post(this.API_PATH + '/change-password', {...changePassword, email});
}
}
......@@ -10,13 +10,13 @@ export const LOAD_CRITERIA_SEARCH_META_FAIL = '[Criteria] Load Criteria Search M
export class LoadCriteriaSearchMetaAction implements Action {
type = LOAD_CRITERIA_SEARCH_META;
constructor(public payload: {} = null) { }
constructor(public payload: string) { }
}
export class LoadCriteriaSearchMetaWipAction implements Action {
type = LOAD_CRITERIA_SEARCH_META_WIP;
constructor(public payload: {} = null) { }
constructor(public payload: string) { }
}
export class LoadCriteriaSearchMetaSuccessAction implements Action {
......
......@@ -10,13 +10,13 @@ export const LOAD_OUTPUT_SEARCH_META_FAIL = '[Output] Load Output Search Meta Fa
export class LoadOutputSearchMetaAction implements Action {
type = LOAD_OUTPUT_SEARCH_META;
constructor(public payload: {} = null) { }
constructor(public payload: string) { }
}
export class LoadOutputSearchMetaWipAction implements Action {
type = LOAD_OUTPUT_SEARCH_META_WIP;
constructor(public payload: {} = null) { }
constructor(public payload: string) { }
}
export class LoadOutputSearchMetaSuccessAction implements Action {
......
......@@ -4,11 +4,12 @@ import { ToastrService } from 'ngx-toastr';
import { Effect, Actions, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { of } from 'rxjs';
import { withLatestFrom, switchMap, map, tap, catchError } from 'rxjs/operators';
import { switchMap, map, tap, catchError } from 'rxjs/operators';
import { Family } from '../model';
import * as criteriaActions from '../action/criteria.action';
import * as fromMetamodel from '../reducers';
import * as fromSearch from '../../search/store/search.reducer';
import { CriteriaService } from '../services/criteria.service';
@Injectable()
......@@ -17,32 +18,27 @@ export class CriteriaEffects {
private actions$: Actions,
private criteriaService: CriteriaService,
private toastr: ToastrService,
private store$: Store<{ metamodel: fromMetamodel.State }>
private store$: Store<{ metamodel: fromMetamodel.State, search: fromSearch.State }>
) { }
@Effect()
loadCriteriaSearchMetaAction$ = this.actions$.pipe(
ofType(criteriaActions.LOAD_CRITERIA_SEARCH_META),
withLatestFrom(this.store$),
switchMap(([action, state]) => {
if (state.metamodel.criteria.criteriaSearchMetaIsLoaded) {
return of({ type: '[No Action] [Criteria] Criteria search meta is already loaded' });
} else {
return of(new criteriaActions.LoadCriteriaSearchMetaWipAction());
}
switchMap((action: criteriaActions.LoadCriteriaSearchMetaAction) => {
return of(new criteriaActions.LoadCriteriaSearchMetaWipAction(action.payload));
})
);
@Effect()
loadCriteriaSearchMetaWipAction$ = this.actions$.pipe(
ofType(criteriaActions.LOAD_CRITERIA_SEARCH_META_WIP),
switchMap(_ =>
this.criteriaService.retrieveCriteriaSearchMeta().pipe(
switchMap((action: criteriaActions.LoadCriteriaSearchMetaWipAction) => {
return this.criteriaService.retrieveCriteriaSearchMeta(action.payload).pipe(
map((datasetSearchMeta: Family[]) =>
new criteriaActions.LoadCriteriaSearchMetaSuccessAction(datasetSearchMeta)),
catchError(() => of(new criteriaActions.LoadCriteriaSearchMetaFailAction()))
)
)
);
})
);
@Effect({ dispatch: false })
......
......@@ -4,11 +4,12 @@ import { ToastrService } from 'ngx-toastr';
import { Effect, Actions, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { of } from 'rxjs';
import { withLatestFrom, switchMap, map, catchError, tap } from 'rxjs/operators';
import { switchMap, map, catchError, tap } from 'rxjs/operators';