Commit baf45ab5 authored by Tifenn Guillas's avatar Tifenn Guillas

Merge branch 'develop' into 'master'

Develop

See merge request !169
parents a25bf4d6 fda5d5df
......@@ -6,7 +6,7 @@ stages:
- deploy
variables:
VERSION: "3.3.0"
VERSION: "3.5.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,26 @@ 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.5.0] - 2020-12
### Added
- #140 => Add description tooltip on search multiple datasets page
- #136 => Add detail view for spectra type object and default object
### Fixed
- #142 => Fix bug datatable: attribute disappeared in specific circonstances
### Changed
- #151 => Display authentication actions buttons if instance configuration allows it
- #145 => Display public datasets only when no user logged in
- #134 => Result search summary into accordion
- #139 => Datasets selected in search multiple is a configurable option in anis-admin
- #138 => Sort attributes and put scrollbar if table too long in Documentation module
- #133 => Change typo if only one dataset
### Security
- #127 => Restrict navigation depending on instance configuration
## [3.4.0] - 2020-10
### Added
- #132 => Add reset cone search button in search multiple
......
3.4.0
\ No newline at end of file
3.5.0
\ No newline at end of file
......@@ -20,6 +20,7 @@
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/silent-renew.html",
"src/favicon.ico",
"src/assets"
],
......
FROM node:13-slim
FROM node:14-slim
ENV DEBIAN_FRONTEND=noninteractive
......
......@@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
......
......@@ -8,6 +8,7 @@ module.exports = function (config) {
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
// require('karma-spec-reporter'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
......@@ -20,7 +21,11 @@ module.exports = function (config) {
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
reporters: [
'progress',
'kjhtml',
// 'spec'
],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
......
......@@ -11,52 +11,56 @@
},
"private": true,
"dependencies": {
"@angular/animations": "~9.1.1",
"@angular/common": "~9.1.1",
"@angular/compiler": "~9.1.1",
"@angular/core": "~9.1.1",
"@angular/forms": "~9.1.1",
"@angular/platform-browser": "~9.1.1",
"@angular/platform-browser-dynamic": "~9.1.1",
"@angular/router": "~9.1.1",
"@fortawesome/fontawesome-free": "^5.13.0",
"@ng-select/ng-select": "^4.0.0",
"@ngrx/effects": "^9.1.0",
"@ngrx/entity": "^9.1.0",
"@ngrx/router-store": "^9.1.0",
"@ngrx/store": "^9.1.0",
"@ngrx/store-devtools": "^9.1.0",
"bootstrap": "^4.4.1",
"@angular/animations": "~10.2.2",
"@angular/common": "~10.2.2",
"@angular/compiler": "~10.2.2",
"@angular/core": "~10.2.2",
"@angular/forms": "~10.2.2",
"@angular/platform-browser": "~10.2.2",
"@angular/platform-browser-dynamic": "~10.2.2",
"@angular/router": "~10.2.2",
"@fortawesome/fontawesome-free": "^5.15.1",
"@ng-select/ng-select": "^5.0.8",
"@ngrx/effects": "^10.0.1",
"@ngrx/entity": "^10.0.1",
"@ngrx/router-store": "^10.0.1",
"@ngrx/store": "^10.0.1",
"@ngrx/store-devtools": "^10.0.1",
"bootstrap": "^4.5.3",
"d3": "^5.15.1",
"ng-inline-svg": "^10.1.0",
"ngx-bootstrap": "^5.6.1",
"jwt-decode": "^3.1.2",
"keycloak-angular": "^8.0.1",
"keycloak-js": "^11.0.3",
"ng-inline-svg": "^11.0.1",
"ngx-bootstrap": "^6.1.0",
"ngx-json-viewer": "^2.4.0",
"ngx-toastr": "^12.0.1",
"rxjs": "~6.5.4",
"tslib": "^1.9.0",
"ngx-toastr": "^13.1.0",
"rxjs": "~6.6.3",
"tslib": "^2.0.0",
"zone.js": "~0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.901.1",
"@angular/cli": "~9.1.1",
"@angular/compiler-cli": "~9.1.1",
"@angular/language-service": "~9.1.1",
"@angular-devkit/build-angular": "~0.1002.0",
"@angular/cli": "~10.2.0",
"@angular/compiler-cli": "~10.2.2",
"@angular/language-service": "~10.2.2",
"@types/d3": "^5.7.2",
"@types/jasmine": "~3.3.8",
"@types/jasmine": "~3.6.1",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.11.1",
"codelyzer": "^5.0.0",
"codelyzer": "^6.0.1",
"intl": "^1.2.5",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.1.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.15.0",
"typescript": "~3.7.5"
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"karma-spec-reporter": "^0.0.32",
"protractor": "~7.0.0",
"ts-node": "~9.0.0",
"tslint": "~6.1.0",
"typescript": "~4.0.5"
}
}
/**
* 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 { NgModule } from '@angular/core';
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
import { NavigationGuard } from './core/navigation.guard';
import { NotFoundPageComponent } from './core/containers/not-found-page.component';
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{
path: 'search',
loadChildren: () => import('./search/search.module').then(m => m.SearchModule)
loadChildren: () => import('./search/search.module').then(m => m.SearchModule),
canActivate: [NavigationGuard]
},
{
path: 'search-multiple',
loadChildren: () => import('./search-multiple/search-multiple.module').then(m => m.SearchMultipleModule)
loadChildren: () => import('./search-multiple/search-multiple.module').then(m => m.SearchMultipleModule),
canActivate: [NavigationGuard]
},
{
path: 'detail/:dname/:objectSelected',
......@@ -19,7 +31,8 @@ const routes: Routes = [
},
{
path: 'documentation',
loadChildren: () => import('./documentation/documentation.module').then(m => m.DocumentationModule)
loadChildren: () => import('./documentation/documentation.module').then(m => m.DocumentationModule),
canActivate: [NavigationGuard]
},
{ path: '**', component: NotFoundPageComponent }
];
......
/**
* 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 { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
......@@ -11,8 +20,8 @@ import { EffectsModule } from '@ngrx/effects';
import { CustomRouterStateSerializer } from './shared/utils';
import { reducers, metaReducers } from './app.reducer';
import { CoreModule } from './core/core.module';
import { AuthModule } from './auth/auth.module';
import { StaticModule } from './static/static.module';
import { LoginModule } from './login/login.module';
import { MetamodelModule } from './metamodel/metamodel.module';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './core/containers/app.component';
......@@ -24,8 +33,8 @@ import { environment } from '../environments/environment';
BrowserAnimationsModule,
HttpClientModule,
CoreModule,
AuthModule,
StaticModule,
LoginModule,
MetamodelModule,
StoreModule.forRoot(reducers, {
metaReducers,
......@@ -45,6 +54,6 @@ import { environment } from '../environments/environment';
!environment.production ? StoreDevtoolsModule.instrument() : [],
EffectsModule.forRoot([])
],
bootstrap: [AppComponent]
bootstrap: [ AppComponent ]
})
export class AppModule { }
/**
* 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 { ActionReducerMap, ActionReducer, MetaReducer } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store';
......
import * as authActions from './auth.action';
import { UserProfile } from './user-profile.model';
describe('[Auth] Action', () => {
it('should create LoginAction', () => {
const action = new authActions.LoginAction();
expect(action.type).toEqual(authActions.LOGIN);
});
it('should create LogoutAction', () => {
const action = new authActions.LogoutAction();
expect(action.type).toEqual(authActions.LOGOUT);
});
it('should create AuthSuccessAction', () => {
const action = new authActions.AuthSuccessAction();
expect(action.type).toEqual(authActions.AUTH_SUCCESS);
});
it('should create LoadUserProfileSuccessAction', () => {
const profile: UserProfile = { id: 'id', username: 'toto' };
const action = new authActions.LoadUserProfileSuccessAction(profile);
expect(action.type).toEqual(authActions.LOAD_USER_PROFILE_SUCCESS);
expect(action.payload).toEqual(profile);
});
it('should create LoadUserRolesSuccessAction', () => {
const action = new authActions.LoadUserRolesSuccessAction(['toto']);
expect(action.type).toEqual(authActions.LOAD_USER_ROLES_SUCCESS);
expect(action.payload).toEqual(['toto']);
});
it('should create OpenEditProfileAction', () => {
const action = new authActions.OpenEditProfileAction();
expect(action.type).toEqual(authActions.OPEN_EDIT_PROFILE);
});
});
/**
* 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 { Action } from '@ngrx/store';
import { UserProfile } from './user-profile.model';
export const LOGIN = '[Auth] Login';
export const LOGOUT = '[Auth] Logout';
export const AUTH_SUCCESS = '[Auth] Auth Success';
export const LOAD_USER_PROFILE_SUCCESS = '[Auth] Load User Profile Success';
export const LOAD_USER_ROLES_SUCCESS = '[Auth] Load User Roles Success';
export const OPEN_EDIT_PROFILE = '[Auth] Open Edit Profile';
/**
* @class
* @classdesc LoginAction action.
* @readonly
*/
export class LoginAction implements Action {
readonly type = LOGIN;
constructor(public payload: {} = null) { }
}
/**
* @class
* @classdesc LogoutAction action.
* @readonly
*/
export class LogoutAction implements Action {
readonly type = LOGOUT;
constructor(public payload: {} = null) { }
}
/**
* @class
* @classdesc AuthSuccessAction action.
* @readonly
*/
export class AuthSuccessAction implements Action {
readonly type = AUTH_SUCCESS;
constructor(public payload: {} = null) { }
}
/**
* @class
* @classdesc LoadUserProfileSuccessAction action.
* @readonly
*/
export class LoadUserProfileSuccessAction implements Action {
readonly type = LOAD_USER_PROFILE_SUCCESS;
constructor(public payload: UserProfile) { }
}
/**
* @class
* @classdesc LoadUserRolesSuccessAction action.
* @readonly
*/
export class LoadUserRolesSuccessAction implements Action {
readonly type = LOAD_USER_ROLES_SUCCESS;
constructor(public payload: string[]) { }
}
/**
* @class
* @classdesc OpenEditProfileAction action.
* @readonly
*/
export class OpenEditProfileAction implements Action {
readonly type = OPEN_EDIT_PROFILE;
constructor(public payload: {} = null) { }
}
export type Actions
= LoginAction
| LogoutAction
| AuthSuccessAction
| LoadUserProfileSuccessAction
| LoadUserRolesSuccessAction
| OpenEditProfileAction;
/**
* 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 { Effect, Actions, ofType } from '@ngrx/effects';
import { from } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { KeycloakService } from 'keycloak-angular';
import * as authActions from './auth.action';
import { environment } from '../../environments/environment';
@Injectable()
/**
* @class
* @classdesc Authentication effects.
*/
export class AuthEffects {
constructor(
private actions$: Actions,
private keycloak: KeycloakService
) { }
/**
* Executes log in.
*/
@Effect({ dispatch: false })
loginAction$ = this.actions$.pipe(
ofType(authActions.LOGIN),
tap(_ => this.keycloak.login())
);
/**
* Executes log out.
*/
@Effect({ dispatch: false })
logoutAction$ = this.actions$.pipe(
ofType(authActions.LOGOUT),
tap(_ => this.keycloak.logout())
);
/**
* Saves user profile and gets user roles.
*/
@Effect()
authSuccessAction$ = this.actions$.pipe(
ofType(authActions.AUTH_SUCCESS),
switchMap(_ =>
from(this.keycloak.loadUserProfile()).pipe(
switchMap(userProfile => [
new authActions.LoadUserProfileSuccessAction(userProfile),
new authActions.LoadUserRolesSuccessAction(this.keycloak.getUserRoles())
])
)
)
);
/**
* Opens edit profile page.
*/
@Effect({ dispatch: false })
OpenEditProfileAction$ = this.actions$.pipe(
ofType(authActions.OPEN_EDIT_PROFILE),
tap(_ => window.open(environment.ssoAuthUrl + '/realms/' + environment.ssoRealm + '/account', '_blank'))
);
}
/**
* 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 { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { KeycloakAngularModule } from 'keycloak-angular';
import { reducer } from './auth.reducer';
import { AuthEffects } from './auth.effects';
import { initializeKeycloakAnis } from './init.keycloak';
@NgModule({
imports: [
KeycloakAngularModule,
StoreModule.forFeature('auth', reducer),
EffectsModule.forFeature([ AuthEffects ])
],
providers: [
initializeKeycloakAnis
]
})
/**
* @class
* @classdesc Authentication module.
*/
export class AuthModule { }
import * as fromAuth from './auth.reducer';
import * as authActions from './auth.action';
import { UserProfile } from './user-profile.model';
describe('[Auth] Reducer', () => {
it('should return init state', () => {
const { initialState } = fromAuth;
const action = {} as authActions.Actions;
const state = fromAuth.reducer(undefined, action);
expect(state).toBe(initialState);
});
it('should set isAuthenticated to true', () => {
const { initialState } = fromAuth;
const action = new authActions.AuthSuccessAction();
const state = fromAuth.reducer(initialState, action);
expect(state.isAuthenticated).toBeTruthy();
expect(state.userProfile).toBeNull();
expect(state.userRoles.length).toEqual(0);
expect(state).not.toEqual(initialState);
});
it('should set userProfile', () => {
const profile: UserProfile = { id: 'id', username: 'toto' };
const { initialState } = fromAuth;
const action = new authActions.LoadUserProfileSuccessAction(profile);
const state = fromAuth.reducer(initialState, action);
expect(state.isAuthenticated).toBeFalsy();
expect(state.userProfile).toEqual(profile);
expect(state.userRoles.length).toEqual(0);
expect(state).not.toEqual(initialState);
});
it('should set userRoles', () => {
const { initialState } = fromAuth;
const action = new authActions.LoadUserRolesSuccessAction(['toto']);
const state = fromAuth.reducer(initialState, action);
expect(state.isAuthenticated).toBeFalsy();
expect(state.userProfile).toBeNull();
expect(state.userRoles.length).toEqual(1);
expect(state.userRoles[0]).toEqual('toto');
expect(state).not.toEqual(initialState);
});