diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e0972a5241ed071536f133a467a0661e0f902c9a
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,35 @@
+stages: 
+    - trigger-child-pipelines
+
+anis-client:
+    stage: trigger-child-pipelines
+    trigger:
+        include: client/.gitlab-ci.yml
+        strategy: depend
+    only:
+        changes:
+            - client/**/*
+        refs:
+            - develop
+
+anis-server:
+    stage: trigger-child-pipelines
+    trigger:
+        include: server/.gitlab-ci.yml
+        strategy: depend
+    only:
+        changes:
+            - server/**/*
+        refs:
+            - develop
+
+anis-services:
+    stage: trigger-child-pipelines
+    trigger:
+        include: services/.gitlab-ci.yml
+        strategy: depend
+    only:
+        changes:
+            - services/**/*
+        refs:
+            - develop
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index bb3cf1c5764313c8e553bacb424b62a30af73ff0..5a52df68a6df724c157b0743657d6c83b7e49a92 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,14 +1,15 @@
-AstroNomical Information System
+AstroNomical Information System - ANIS
   
+  website: https://anis.lam.fr
   Copyright: CNRS - 2021
-  Address:  Centre de donneeS Astrophysique de Marseille (CeSAM)              
+  Address:  Centre de donnéeS Astrophysique de Marseille (CeSAM)              
             Laboratoire d'Astrophysique de Marseille                          
-            Ple de l'Etoile, site de Chteau-Gombert                         
-            38, rue Frdric Joliot-Curie                                     
+            Pôle de l'Etoile, site de Château-Gombert                         
+            38, rue Frédéric Joliot-Curie                                     
             13388 Marseille cedex 13 France                                   
             CNRS U.M.R 7326
 
-Anis Server is governed by the CeCILL license under French law and
+ANIS is governed by the CeCILL license under French law and
 abiding by the rules of distribution of free software.  You can  use, 
 modify and/ or redistribute the software under the terms of the CeCILL
 license as circulated by CEA, CNRS and INRIA at the following URL
diff --git a/Makefile b/Makefile
index 0368837d826004435fc49f42ac9940da72657ad0..be40c4535f47f42898b06fcd34a33ef361223216 100644
--- a/Makefile
+++ b/Makefile
@@ -15,6 +15,7 @@ list:
 	@echo "  shell_client     > shell into angular client container"
 	@echo "  build_client     > generate the angular client dist application (html, css, js)"
 	@echo "  test_client      > Starts the angular client unit tests"
+	@echo "  client_coverage  > Run nginx web server test client coverage"
 	@echo "  install_server   > install server dependencies"
 	@echo "  shell_server     > shell into php server container"
 	@echo "  test_server      > Starts the server php unit tests"
@@ -54,7 +55,10 @@ build_client:
 	@docker-compose exec client ng build
 
 test_client:
-	@docker-compose exec client ng test --no-watch --code-coverage
+	@docker-compose exec client npx jest
+
+client_coverage:
+	@docker run --name anis-client-code-coverage -d -p 8888:80 -v $(CURDIR)/client/coverage/anis-client:/usr/share/nginx/html:ro nginx
 
 install_server:
 	@docker run --init -it --rm --user $(UID):$(GID) \
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..c68157c1a6a96fd9bee8def6b8dc7b32d86adaac
--- /dev/null
+++ b/README.md
@@ -0,0 +1,47 @@
+# AstroNomical Information System - ANIS 
+
+## Introduction
+
+AstroNomical Information System is a generic web tool that aims to facilitate
+the provision of data (Astrophysics), accessible from a database, for the scientific 
+community.
+
+This software allows you to control one or more databases related to astronomical 
+projects and allows access to datasets via a web interface or URLs.
+
+Anis is protected by the CeCILL licence (see LICENCE file at the software root).
+
+## Authors
+
+Here is the list of people involved in the development:
+
+* `François Agneray` : Laboratoire d'Astrophysique de Marseille (CNRS)
+* `Chrystel Moreau` : Laboratoire d'Astrophysique de Marseille (CNRS)
+* `Tifenn Guillas` : Laboratoire d'Astrophysique de Marseille (CNRS)
+
+## More resources:
+
+* [Website](https://anis.lam.fr)
+* [Documentation](https://anis.lam.fr/doc/)
+
+## Installing and starting the application
+
+Anis Server contains a Makefile that helps the developer to install and start the application.
+
+To list all operations availables just type `make` in your terminal at the root of this application.
+
+- To install client dependancies: `make install_client`
+- To install server dependancies: `make install_server`
+- To build or rebuild all docker images and start containers: `make rebuild`
+- To start/stop/restart/status all services: `make start|stop|restart|status`
+- To display logs for all services: `make logs`
+- To open a shell command into client container: `make shell_client`
+- To open a shell command into server container: `make shell_server`
+- To execute server tests suite: `make test_server`
+- To execute php code sniffer: `make phpcs`
+- To create the metamodel database: `make create-db`
+- TO remove the metadata database: `make remove-pgdata`
+
+## Web interface
+
+In development mode the web interface can be accessed at the following url : http://localhost:4200
diff --git a/client/.gitlab-ci.yml b/client/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f803db047618684ee122a8bb17322c98f4d9dd8f
--- /dev/null
+++ b/client/.gitlab-ci.yml
@@ -0,0 +1,83 @@
+stages:
+    - install_dependencies
+    - test
+    - sonar
+    - build
+    - dockerize
+
+variables:
+    VERSION: "3.7.0"
+    SONARQUBE_URL: https://sonarqube.lam.fr
+
+install_dependencies:
+    image: node:16-slim
+    stage: install_dependencies
+    cache: 
+        key: ${CI_COMMIT_REF_SLUG}_client
+        paths:
+            - client/node_modules
+        policy: pull-push
+    script:
+        - cd client
+        - yarn install
+
+test:
+    image: node:16-slim
+    stage: test
+    cache: 
+        key: ${CI_COMMIT_REF_SLUG}_client
+        paths:
+            - node_modules
+        policy: pull
+    script:
+        - cd client
+        - npx jest --coverage --coverageReporters lcov 
+    artifacts:
+        paths:
+            - client/coverage
+
+sonar_scanner:
+    image: sonarsource/sonar-scanner-cli:latest
+    stage: sonar
+    cache: 
+        key: ${CI_COMMIT_REF_SLUG}_client
+        paths:
+            - client/node_modules
+        policy: pull
+    script:
+        - cd client
+        - sonar-scanner
+            -Dsonar.projectKey=anis-client
+            -Dsonar.sources=src
+            -Dsonar.projectVersion=$VERSION 
+            -Dsonar.host.url=$SONARQUBE_URL 
+            -Dsonar.login=$SONAR_TOKEN_CLIENT
+            -Dsonar.exclusions=**.spec.ts
+            -Dsonar.typescript.lcov.reportPaths=./coverage/anis-client/lcov.info
+
+build:
+    image: node:16-slim
+    stage: build
+    cache: 
+        key: ${CI_COMMIT_REF_SLUG}_client
+        paths:
+            - client/node_modules
+        policy: pull
+    script:
+        - cd client
+        - yarn global add @angular/cli@latest
+        - ng build
+    artifacts:
+        paths:
+            - client/dist
+
+dockerize:
+    image: docker:stable
+    stage: dockerize
+    cache: {}
+    script:
+        - cd client
+        - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+        - docker pull $CI_REGISTRY/anis/anis-next/client:latest || true
+        - docker build --cache-from $CI_REGISTRY/anis/anis-next/client:latest -t $CI_REGISTRY/anis/anis-next/client:latest .
+        - docker push $CI_REGISTRY/anis/anis-next/client:latest
diff --git a/client/angular.json b/client/angular.json
index 01838b184b3b0e34d8a5060e94bc566d373890d8..17efce44ec943e8de2e16019742c49a058247df6 100644
--- a/client/angular.json
+++ b/client/angular.json
@@ -89,24 +89,6 @@
           "options": {
             "browserTarget": "client:build"
           }
-        },
-        "test": {
-          "builder": "@angular-devkit/build-angular:karma",
-          "options": {
-            "main": "src/test.ts",
-            "polyfills": "src/polyfills.ts",
-            "tsConfig": "tsconfig.spec.json",
-            "karmaConfig": "karma.conf.js",
-            "inlineStyleLanguage": "scss",
-            "assets": [
-              "src/favicon.ico",
-              "src/assets"
-            ],
-            "styles": [
-              "src/styles.scss"
-            ],
-            "scripts": []
-          }
         }
       }
     }
diff --git a/client/jest.config.js b/client/jest.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..d23d9551d318b95afc421fc394c4f3acb970d8f6
--- /dev/null
+++ b/client/jest.config.js
@@ -0,0 +1,16 @@
+const { pathsToModuleNameMapper } = require('ts-jest/utils');
+const { compilerOptions } = require('./tsconfig');
+
+module.exports = {
+    preset: 'jest-preset-angular',
+    testMatch: ['**/+(*.)+(spec).+(ts|js)'],
+    setupFilesAfterEnv: ['<rootDir>/src/test.ts'],
+    collectCoverage: false,
+    collectCoverageFrom: ['src/**/*.ts'],
+    coverageReporters: ['html'],
+    coverageDirectory: 'coverage/anis-client',
+    moduleDirectories: ["node_modules", "./"],
+    moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths || {}, {
+        prefix: '<rootDir>/',
+    }),
+};
diff --git a/client/karma.conf.js b/client/karma.conf.js
deleted file mode 100644
index c6a90c10a8ee5c516f491e1bb1b2acc24763b9d0..0000000000000000000000000000000000000000
--- a/client/karma.conf.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// Karma configuration file, see link for more information
-// https://karma-runner.github.io/1.0/config/configuration-file.html
-
-module.exports = function (config) {
-  config.set({
-    basePath: '',
-    frameworks: ['jasmine', '@angular-devkit/build-angular'],
-    plugins: [
-      require('karma-jasmine'),
-      require('karma-chrome-launcher'),
-      require('karma-jasmine-html-reporter'),
-      require('karma-coverage'),
-      require('@angular-devkit/build-angular/plugins/karma')
-    ],
-    client: {
-      jasmine: {
-        // you can add configuration options for Jasmine here
-        // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
-        // for example, you can disable the random execution with `random: false`
-        // or set a specific seed with `seed: 4321`
-      },
-      clearContext: false // leave Jasmine Spec Runner output visible in browser
-    },
-    jasmineHtmlReporter: {
-      suppressAll: true // removes the duplicated traces
-    },
-    coverageReporter: {
-      dir: require('path').join(__dirname, './coverage/client'),
-      subdir: '.',
-      reporters: [
-        { type: 'html' },
-        { type: 'text-summary' }
-      ]
-    },
-    reporters: ['progress', 'kjhtml'],
-    port: 9876,
-    colors: true,
-    logLevel: config.LOG_INFO,
-    autoWatch: true,
-    browsers: ['Chrome'],
-    singleRun: false,
-    restartOnFileChange: true
-  });
-};
diff --git a/client/package.json b/client/package.json
index 7e7cad96734364427fdcc7fe3022f1e64f022190..1069d167dc4188e3a4e06442bc7e19522d57a83f 100644
--- a/client/package.json
+++ b/client/package.json
@@ -5,8 +5,7 @@
     "ng": "ng",
     "start": "ng serve",
     "build": "ng build",
-    "watch": "ng build --watch --configuration development",
-    "test": "ng test"
+    "watch": "ng build --watch --configuration development"
   },
   "private": true,
   "dependencies": {
@@ -26,6 +25,7 @@
     "@ngrx/store": "12.1.0",
     "@ngrx/store-devtools": "12.1.0",
     "bootstrap": "4.6",
+    "d3": "^5.15.1",
     "keycloak-angular": "^8.2.0",
     "keycloak-js": "^14.0.0",
     "ngx-bootstrap": "^7.0.0-rc.1",
@@ -39,14 +39,13 @@
     "@angular-devkit/build-angular": "~12.0.4",
     "@angular/cli": "~12.0.4",
     "@angular/compiler-cli": "~12.0.4",
+    "@types/d3": "^5.7.2",
     "@types/jasmine": "~3.6.0",
+    "@types/jest": "^26.0.24",
     "@types/node": "^12.11.1",
     "jasmine-core": "~3.7.0",
-    "karma": "~6.3.0",
-    "karma-chrome-launcher": "~3.1.0",
-    "karma-coverage": "~2.0.3",
-    "karma-jasmine": "~4.0.0",
-    "karma-jasmine-html-reporter": "^1.5.0",
+    "jest": "^27.0.6",
+    "jest-preset-angular": "^9.0.5",
     "typescript": "~4.2.3"
   }
 }
diff --git a/client/src/app/admin/admin-auth.guard.ts b/client/src/app/admin/admin-auth.guard.ts
new file mode 100644
index 0000000000000000000000000000000000000000..02ac7a5e6dea1d5f36f8abb33b1a912d2c75d9e5
--- /dev/null
+++ b/client/src/app/admin/admin-auth.guard.ts
@@ -0,0 +1,59 @@
+/**
+ * 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 { CanActivate, Router } from '@angular/router';
+import { Store, select } from '@ngrx/store';
+
+import { combineLatest, Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import * as authActions from 'src/app/auth/auth.actions';
+import * as authSelector from 'src/app/auth/auth.selector';
+import { AppConfigService } from 'src/app/app-config.service';
+
+@Injectable({
+    providedIn: 'root',
+})
+export class AdminAuthGuard implements CanActivate {
+    constructor(
+        protected readonly router: Router,
+        private store: Store<{ }>,
+        private config: AppConfigService
+    ) { }
+
+    canActivate(): Observable<boolean> {
+        return combineLatest([
+            this.store.pipe(select(authSelector.selectUserRoles)),
+            this.store.pipe(select(authSelector.selectIsAuthenticated))
+        ]).pipe(
+            map(([userRoles, isAuthenticated]) => {
+                // Auth disabled
+                if (!this.config.authenticationEnabled) {
+                    return true;
+                }
+
+                // Force the user to log in if currently unauthenticated.
+                if (!isAuthenticated) {
+                    this.store.dispatch(authActions.login());
+                    return false;
+                }
+
+                // If authenticated but not admin go to unauthorized page.
+                if (!userRoles.includes(this.config.adminRole)) {
+                    this.router.navigateByUrl('/unauthorized');
+                    return false;
+                }
+
+                // Let "Router" allow user entering the page
+                return true;
+            })
+        );
+    }
+}
diff --git a/client/src/app/admin/admin-routing.module.ts b/client/src/app/admin/admin-routing.module.ts
index 6fa41e51897b04d580ea504f2da972238d9eed53..d3aef6a6b6acd59c9bd6da9d15e7d601e3b34eeb 100644
--- a/client/src/app/admin/admin-routing.module.ts
+++ b/client/src/app/admin/admin-routing.module.ts
@@ -11,6 +11,7 @@ import { NgModule } from '@angular/core';
 import { RouterModule, Routes } from '@angular/router';
 
 import { AdminComponent } from './admin.component';
+import { AdminAuthGuard } from './admin-auth.guard';
 import { InstanceListComponent } from './containers/instance/instance-list.component';
 import { NewInstanceComponent } from './containers/instance/new-instance.component';
 import { EditInstanceComponent } from './containers/instance/edit-instance.component';
@@ -31,7 +32,7 @@ import { SettingsComponent } from './containers/settings/settings.component';
 
 const routes: Routes = [
     { 
-        path: 'admin', component: AdminComponent, children: [
+        path: '', component: AdminComponent, canActivate: [AdminAuthGuard], children: [
             { path: '', redirectTo: 'instance-list', pathMatch: 'full' },
             { path: 'instance-list', component: InstanceListComponent },
             { path: 'new-instance', component: NewInstanceComponent },
@@ -56,7 +57,7 @@ const routes: Routes = [
 ];
 
 @NgModule({
-    imports: [RouterModule.forRoot(routes)],
+    imports: [RouterModule.forChild(routes)],
     exports: [RouterModule]
 })
 export class AdminRoutingModule { }
diff --git a/client/src/app/admin/admin.component.html b/client/src/app/admin/admin.component.html
index 979df50e47db62138793be21dff9adf7df5e4cf1..61101934f1e33d3d27823eb8998cedcf423d891b 100644
--- a/client/src/app/admin/admin.component.html
+++ b/client/src/app/admin/admin.component.html
@@ -3,6 +3,8 @@
         [links]="links"
         [isAuthenticated]="isAuthenticated | async"
         [userProfile]="userProfile | async"
+        [baseHref]="getBaseHref()"
+        [authenticationEnabled]="getAuthenticationEnabled()"
         (login)="login()"
         (logout)="logout()"
         (openEditProfile)="openEditProfile()">
diff --git a/client/src/app/admin/admin.component.ts b/client/src/app/admin/admin.component.ts
index a66e4f324e6afb0e123c825b0e06dd1ec73d195d..93e97de10f1e36607d482147e840a4567f1e5e4c 100644
--- a/client/src/app/admin/admin.component.ts
+++ b/client/src/app/admin/admin.component.ts
@@ -14,6 +14,7 @@ import { Store } from '@ngrx/store';
 import { UserProfile } from 'src/app/auth/user-profile.model';
 import * as authActions from 'src/app/auth/auth.actions';
 import * as authSelector from 'src/app/auth/auth.selector';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Component({
     selector: 'app-admin',
@@ -27,7 +28,7 @@ import * as authSelector from 'src/app/auth/auth.selector';
  */
 export class AdminComponent {
     public links = [
-        { label: 'Back to portal', icon: 'fas fa-level-up-alt', routerLink: '/portal-home' },
+        { label: 'Back to portal', icon: 'fas fa-level-up-alt', routerLink: '/portal' },
         { label: 'Instances', icon: 'fas fa-object-group', routerLink: 'instance-list' },
         { label: 'Surveys', icon: 'fas fa-table', routerLink: 'survey-list'},
         { label: 'Databases', icon: 'fas fa-database', routerLink: 'database-list'},
@@ -37,12 +38,20 @@ export class AdminComponent {
     public userProfile: Observable<UserProfile>;
     public userRoles: Observable<string[]>;
 
-    constructor(private store: Store<{ }>) {
+    constructor(private store: Store<{ }>, private config: AppConfigService) {
         this.isAuthenticated = store.select(authSelector.selectIsAuthenticated);
         this.userProfile = store.select(authSelector.selectUserProfile);
         this.userRoles = store.select(authSelector.selectUserRoles);
     }
 
+    getBaseHref() {
+        return this.config.baseHref;
+    }
+
+    getAuthenticationEnabled() {
+        return this.config.authenticationEnabled;
+    }
+
     login(): void {
         this.store.dispatch(authActions.login());
     }
diff --git a/client/src/app/admin/components/attribute/add-attribute.component.ts b/client/src/app/admin/components/attribute/add-attribute.component.ts
index f7934d61f05212f998fcdcf66c10556de52e0046..fc050468a347ea1eb1ded1d298b01fdcbb1fd24b 100644
--- a/client/src/app/admin/components/attribute/add-attribute.component.ts
+++ b/client/src/app/admin/components/attribute/add-attribute.component.ts
@@ -1,3 +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 { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, TemplateRef } from '@angular/core';
 
 import { BsModalService } from 'ngx-bootstrap/modal';
diff --git a/client/src/app/admin/components/attribute/criteria/generate-option-list.component.ts b/client/src/app/admin/components/attribute/criteria/generate-option-list.component.ts
index 1b6905a2bbcd1f55707332bf561aa085ea78005d..cc0899ce9fea358fc0b06e4a8fc576a06518e951 100644
--- a/client/src/app/admin/components/attribute/criteria/generate-option-list.component.ts
+++ b/client/src/app/admin/components/attribute/criteria/generate-option-list.component.ts
@@ -1,3 +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 { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, TemplateRef } from '@angular/core';
 
 import { BsModalService } from 'ngx-bootstrap/modal';
diff --git a/client/src/app/admin/components/attribute/criteria/option-form.component.ts b/client/src/app/admin/components/attribute/criteria/option-form.component.ts
index 5e2a620d0a6985db4690cf84108b4e59b85d2ee2..cff2bdcdf35a3401fad6adf163e80240c75cdd84 100644
--- a/client/src/app/admin/components/attribute/criteria/option-form.component.ts
+++ b/client/src/app/admin/components/attribute/criteria/option-form.component.ts
@@ -1,3 +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 { Component, Input, ChangeDetectionStrategy } from '@angular/core';
 import { FormGroup } from '@angular/forms';
 
diff --git a/client/src/app/admin/components/attribute/criteria/option-list.component.ts b/client/src/app/admin/components/attribute/criteria/option-list.component.ts
index 492bac069a3bfccdd9e8a9f3846a168651c1281b..c3e643a5a7c5cf9b145970aeb5853227a86f1139 100644
--- a/client/src/app/admin/components/attribute/criteria/option-list.component.ts
+++ b/client/src/app/admin/components/attribute/criteria/option-list.component.ts
@@ -1,3 +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 { Component, Input, OnInit, Output, EventEmitter } from '@angular/core';
 import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
 
diff --git a/client/src/app/admin/components/attribute/criteria/table-criteria.component.ts b/client/src/app/admin/components/attribute/criteria/table-criteria.component.ts
index fe5c7db6f47a4db658929ac124354620ee5286f4..9cf5e6dea58fc2c086408e5bbba425aea966a31b 100644
--- a/client/src/app/admin/components/attribute/criteria/table-criteria.component.ts
+++ b/client/src/app/admin/components/attribute/criteria/table-criteria.component.ts
@@ -1,3 +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 { Component } from '@angular/core';
 
 @Component({
diff --git a/client/src/app/admin/components/attribute/criteria/tr-criteria.component.ts b/client/src/app/admin/components/attribute/criteria/tr-criteria.component.ts
index 87109e23f306817016211fcf8c23f33ee25ba656..9c1f559f4c038fd74a28174071260afdd97c489d 100644
--- a/client/src/app/admin/components/attribute/criteria/tr-criteria.component.ts
+++ b/client/src/app/admin/components/attribute/criteria/tr-criteria.component.ts
@@ -1,3 +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 { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit } from '@angular/core';
 import { FormArray, FormControl, FormGroup } from '@angular/forms';
 
@@ -6,7 +15,7 @@ import { Attribute, Option, CriteriaFamily, SelectOption } from 'src/app/metamod
 @Component({
     selector: '[criteria]',
     templateUrl: 'tr-criteria.component.html',
-    styleUrls: [ '../tr.component.css' ],
+    styleUrls: [ '../tr.component.scss' ],
     changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class TrCriteriaComponent implements OnInit {
diff --git a/client/src/app/admin/components/attribute/design/index.ts b/client/src/app/admin/components/attribute/design/index.ts
index 97b51ae6380e1ab54108323c607878012b8500c0..e2a18cde45ee5821f4cbbdff63ac2ebf00d12480 100644
--- a/client/src/app/admin/components/attribute/design/index.ts
+++ b/client/src/app/admin/components/attribute/design/index.ts
@@ -1,3 +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 { TableDesignComponent } from './table-design.component';
 import { TrDesignComponent } from './tr-design.component';
 
diff --git a/client/src/app/admin/components/attribute/design/table-design.component.ts b/client/src/app/admin/components/attribute/design/table-design.component.ts
index ece9403ec5149fcac9cddc7211434f8cbbb49ee1..4ca4666de803c2c57e84ff169e20140aad1b5861 100644
--- a/client/src/app/admin/components/attribute/design/table-design.component.ts
+++ b/client/src/app/admin/components/attribute/design/table-design.component.ts
@@ -1,3 +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 { Component } from '@angular/core';
 
 @Component({
diff --git a/client/src/app/admin/components/attribute/design/tr-design.component.ts b/client/src/app/admin/components/attribute/design/tr-design.component.ts
index 72d1dcedc79ed6c4bde11309d5be0cb3db93df8e..f9a42f6fac4e54b93513e28e5a1365d3747cea2c 100644
--- a/client/src/app/admin/components/attribute/design/tr-design.component.ts
+++ b/client/src/app/admin/components/attribute/design/tr-design.component.ts
@@ -1,3 +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 { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit } from '@angular/core';
 import { FormControl, FormGroup, Validators } from '@angular/forms';
 
@@ -6,7 +15,7 @@ import { Attribute, SelectOption } from 'src/app/metamodel/models';
 @Component({
     selector: '[design]',
     templateUrl: 'tr-design.component.html',
-    styleUrls: [ '../tr.component.css' ],
+    styleUrls: [ '../tr.component.scss' ],
     changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class TrDesignComponent implements OnInit {
diff --git a/client/src/app/admin/components/attribute/detail/index.ts b/client/src/app/admin/components/attribute/detail/index.ts
index 41a27ae41042126954ad127f19255e1f7f4428ea..d1581ea5b138546e26d7eba21a7cd6d624ce1612 100644
--- a/client/src/app/admin/components/attribute/detail/index.ts
+++ b/client/src/app/admin/components/attribute/detail/index.ts
@@ -1,3 +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 { TableDetailComponent } from './table-detail.component';
 import { TrDetailComponent } from './tr-detail.component';
 
diff --git a/client/src/app/admin/components/attribute/detail/table-detail.component.ts b/client/src/app/admin/components/attribute/detail/table-detail.component.ts
index 5ff070ee6bd57d8971192ca8ec16a1ac16f62e7e..f13fc516276e4de6ed864b1473db48c86d11ced8 100644
--- a/client/src/app/admin/components/attribute/detail/table-detail.component.ts
+++ b/client/src/app/admin/components/attribute/detail/table-detail.component.ts
@@ -1,3 +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 { Component } from '@angular/core';
 
 @Component({
diff --git a/client/src/app/admin/components/attribute/detail/tr-detail.component.ts b/client/src/app/admin/components/attribute/detail/tr-detail.component.ts
index 2f268277c8ef10cca4a9d1e89afc7871c9fe552b..d485de9dbcd7c5aa3a5b15fa4b3a46824ee3a114 100644
--- a/client/src/app/admin/components/attribute/detail/tr-detail.component.ts
+++ b/client/src/app/admin/components/attribute/detail/tr-detail.component.ts
@@ -1,3 +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 { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit } from '@angular/core';
 import { FormControl, FormGroup } from '@angular/forms';
 
@@ -6,7 +15,7 @@ import { Attribute, SelectOption } from 'src/app/metamodel/models';
 @Component({
     selector: '[detail]',
     templateUrl: 'tr-detail.component.html',
-    styleUrls: [ '../tr.component.css' ],
+    styleUrls: [ '../tr.component.scss' ],
     changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class TrDetailComponent implements OnInit {
diff --git a/client/src/app/admin/components/attribute/index.ts b/client/src/app/admin/components/attribute/index.ts
index 4d35bcb434eb4bca8dce8b0d06c48966cb889285..fc2f19be83072e0b31e0947c6cacdeeca9ceb864 100644
--- a/client/src/app/admin/components/attribute/index.ts
+++ b/client/src/app/admin/components/attribute/index.ts
@@ -1,3 +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 { AddAttributeComponent } from './add-attribute.component';
 import { designComponents } from './design';
 import { criteriaComponents } from './criteria';
diff --git a/client/src/app/admin/components/attribute/output/index.ts b/client/src/app/admin/components/attribute/output/index.ts
index b5f11cda5869b5136f7e7f02e13f851565f60ef0..1c915eff1cc2d578dd535dd7e6ee51451e80c72b 100644
--- a/client/src/app/admin/components/attribute/output/index.ts
+++ b/client/src/app/admin/components/attribute/output/index.ts
@@ -1,3 +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 { TableOutputComponent } from "./table-output.component";
 import { TrOutputComponent } from "./tr-output.component";
 
diff --git a/client/src/app/admin/components/attribute/output/table-output.component.ts b/client/src/app/admin/components/attribute/output/table-output.component.ts
index 30c36b2babb7851963e557fd76ec19233995529b..a2aefb31b4810d8402e43fdbc87c8f3e4ad21037 100644
--- a/client/src/app/admin/components/attribute/output/table-output.component.ts
+++ b/client/src/app/admin/components/attribute/output/table-output.component.ts
@@ -1,3 +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 { Component } from '@angular/core';
 
 @Component({
diff --git a/client/src/app/admin/components/attribute/output/tr-output.component.ts b/client/src/app/admin/components/attribute/output/tr-output.component.ts
index a2a7afc994a2711a2acfe186f08b95cb49a85545..f75c845210810abaeaac0395598cb18f9aa2d784 100644
--- a/client/src/app/admin/components/attribute/output/tr-output.component.ts
+++ b/client/src/app/admin/components/attribute/output/tr-output.component.ts
@@ -1,3 +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 { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit } from '@angular/core';
 import { FormControl, FormGroup } from '@angular/forms';
 
@@ -6,7 +15,7 @@ import { Attribute, OutputCategory } from 'src/app/metamodel/models';
 @Component({
     selector: '[output]',
     templateUrl: 'tr-output.component.html',
-    styleUrls: [ '../tr.component.css' ],
+    styleUrls: [ '../tr.component.scss' ],
     changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class TrOutputComponent implements OnInit {
diff --git a/client/src/app/admin/components/attribute/result/index.ts b/client/src/app/admin/components/attribute/result/index.ts
index 4b25f427fd813bfca6ca81a5bf44c4188f42b4e7..3517973ab6dd103644a98387970d4f37872b6c94 100644
--- a/client/src/app/admin/components/attribute/result/index.ts
+++ b/client/src/app/admin/components/attribute/result/index.ts
@@ -1,3 +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 { TableResultComponent } from './table-result.component';
 import { TrResultComponent } from './tr-result.component';
 import { renderers } from './renderers';
diff --git a/client/src/app/admin/components/attribute/result/renderers/detail-renderer.component.ts b/client/src/app/admin/components/attribute/result/renderers/detail-renderer.component.ts
index 2939aecf082237ddfa821aaa4f28b3b85a09d858..4dfc1917776660fa01cef50275fcd0a25e960abd 100644
--- a/client/src/app/admin/components/attribute/result/renderers/detail-renderer.component.ts
+++ b/client/src/app/admin/components/attribute/result/renderers/detail-renderer.component.ts
@@ -1,3 +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 { Component, Input, ChangeDetectionStrategy } from '@angular/core';
 import { FormGroup } from '@angular/forms';
 
diff --git a/client/src/app/admin/components/attribute/result/renderers/download-renderer.component.ts b/client/src/app/admin/components/attribute/result/renderers/download-renderer.component.ts
index ef1fc0d8dca863f65ac15a0e18d0a87784ea26a6..f3ccfee56acdfe319c922b94a050dc2016f4dbf9 100644
--- a/client/src/app/admin/components/attribute/result/renderers/download-renderer.component.ts
+++ b/client/src/app/admin/components/attribute/result/renderers/download-renderer.component.ts
@@ -1,3 +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 { Component, Input, ChangeDetectionStrategy } from '@angular/core';
 import { FormGroup } from '@angular/forms';
 
diff --git a/client/src/app/admin/components/attribute/result/renderers/image-renderer.component.ts b/client/src/app/admin/components/attribute/result/renderers/image-renderer.component.ts
index 8706711543b54f65eeb80158f5869d8bc2df6fcb..1441a6a8d02d150de426e338c213f575f906b44e 100644
--- a/client/src/app/admin/components/attribute/result/renderers/image-renderer.component.ts
+++ b/client/src/app/admin/components/attribute/result/renderers/image-renderer.component.ts
@@ -1,3 +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 { Component, Input, ChangeDetectionStrategy } from '@angular/core';
 import { FormGroup } from '@angular/forms';
 
diff --git a/client/src/app/admin/components/attribute/result/renderers/index.ts b/client/src/app/admin/components/attribute/result/renderers/index.ts
index bf18afa1c74e0390e22d1d91b826378f7b7fcda2..89c8ed9df366bfeacc2b2d3d5c79a16bb6602c32 100644
--- a/client/src/app/admin/components/attribute/result/renderers/index.ts
+++ b/client/src/app/admin/components/attribute/result/renderers/index.ts
@@ -1,3 +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 { DetailRendererComponent } from './detail-renderer.component';
 import { DownloadRendererComponent } from './download-renderer.component';
 import { ImageRendererComponent } from './image-renderer.component';
diff --git a/client/src/app/admin/components/attribute/result/renderers/link-renderer.component.ts b/client/src/app/admin/components/attribute/result/renderers/link-renderer.component.ts
index a67b880373e3a77de5b3dc03415a095110cd315d..d8d136372b8eaec7e50143324d0f0102ffdeb749 100644
--- a/client/src/app/admin/components/attribute/result/renderers/link-renderer.component.ts
+++ b/client/src/app/admin/components/attribute/result/renderers/link-renderer.component.ts
@@ -1,3 +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 { Component, Input, ChangeDetectionStrategy } from '@angular/core';
 import { FormGroup } from '@angular/forms';
 
diff --git a/client/src/app/admin/components/attribute/result/renderers/renderer-form-factory.ts b/client/src/app/admin/components/attribute/result/renderers/renderer-form-factory.ts
index 8a6e7ef3472d123076d87e9fbe2dde4610e75dd0..22c2f5e30eb422a5fbfc7857615f03988ec254cb 100644
--- a/client/src/app/admin/components/attribute/result/renderers/renderer-form-factory.ts
+++ b/client/src/app/admin/components/attribute/result/renderers/renderer-form-factory.ts
@@ -1,3 +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 { FormControl } from "@angular/forms";
 
 export abstract class RendererFormFactory {
diff --git a/client/src/app/admin/components/attribute/result/table-result.component.ts b/client/src/app/admin/components/attribute/result/table-result.component.ts
index d4aa11035eae37eba4bfe62451039fb2175a79b0..86ce85cb110aea655b48802702f6fa3e1b6cff98 100644
--- a/client/src/app/admin/components/attribute/result/table-result.component.ts
+++ b/client/src/app/admin/components/attribute/result/table-result.component.ts
@@ -1,3 +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 { Component } from '@angular/core';
 
 @Component({
diff --git a/client/src/app/admin/components/attribute/result/tr-result.component.ts b/client/src/app/admin/components/attribute/result/tr-result.component.ts
index 38ae002881cd5b1acc23d3e380b1febeca96df19..fe0aa76ee37d653c1ac16cfb3137b0a3cd59d67b 100644
--- a/client/src/app/admin/components/attribute/result/tr-result.component.ts
+++ b/client/src/app/admin/components/attribute/result/tr-result.component.ts
@@ -1,3 +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 { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit } from '@angular/core';
 import { FormControl, FormGroup } from '@angular/forms';
 
@@ -7,7 +16,7 @@ import { RendererFormFactory } from './renderers/renderer-form-factory';
 @Component({
     selector: '[result]',
     templateUrl: 'tr-result.component.html',
-    styleUrls: [ '../tr.component.css' ],
+    styleUrls: [ '../tr.component.scss' ],
     changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class TrResultComponent implements OnInit {
diff --git a/client/src/app/admin/components/attribute/tr.component.css b/client/src/app/admin/components/attribute/tr.component.scss
similarity index 100%
rename from client/src/app/admin/components/attribute/tr.component.css
rename to client/src/app/admin/components/attribute/tr.component.scss
diff --git a/client/src/app/admin/components/attribute/vo/index.ts b/client/src/app/admin/components/attribute/vo/index.ts
index 8b6aa3b64ad95dba7b0ad4323b6f7af7583ad06e..80eb8323179ac219829e543e6319e9c06d040240 100644
--- a/client/src/app/admin/components/attribute/vo/index.ts
+++ b/client/src/app/admin/components/attribute/vo/index.ts
@@ -1,3 +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 { TableVoComponent } from './table-vo.component';
 import { TrVoComponent } from './tr-vo.component';
 
diff --git a/client/src/app/admin/components/attribute/vo/table-vo.component.ts b/client/src/app/admin/components/attribute/vo/table-vo.component.ts
index ba4352e033e9f7ef300d331b041dd005dafd6b51..57df81c8f7f727ff283bd08e1732d19948e55a4b 100644
--- a/client/src/app/admin/components/attribute/vo/table-vo.component.ts
+++ b/client/src/app/admin/components/attribute/vo/table-vo.component.ts
@@ -1,3 +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 { Component } from '@angular/core';
 
 @Component({
diff --git a/client/src/app/admin/components/attribute/vo/tr-vo.component.ts b/client/src/app/admin/components/attribute/vo/tr-vo.component.ts
index 521725061e70a4774a6f9658eac64c8787f127f1..e0dd9bb2fa25492df131aed1335612316a594ec8 100644
--- a/client/src/app/admin/components/attribute/vo/tr-vo.component.ts
+++ b/client/src/app/admin/components/attribute/vo/tr-vo.component.ts
@@ -1,3 +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 { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit } from '@angular/core';
 import { FormControl, FormGroup } from '@angular/forms';
 
@@ -6,7 +15,7 @@ import { Attribute } from 'src/app/metamodel/models';
 @Component({
     selector: '[vo]',
     templateUrl: 'tr-vo.component.html',
-    styleUrls: [ '../tr.component.css' ],
+    styleUrls: [ '../tr.component.scss' ],
     changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class TrVoComponent implements OnInit {
diff --git a/client/src/app/app-config.service.ts b/client/src/app/app-config.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5944e78a5bc3b8821afd91d1a3b8bc431d643157
--- /dev/null
+++ b/client/src/app/app-config.service.ts
@@ -0,0 +1,13 @@
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class AppConfigService {
+    public apiUrl: string;
+    public servicesUrl: string;
+    public baseHref: string;
+    public authenticationEnabled: boolean;
+    public ssoAuthUrl: string;
+    public ssoRealm: string;
+    public ssoClientId: string;
+    public adminRole: string;
+}
diff --git a/client/src/app/app-init.ts b/client/src/app/app-init.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b3dc042681bc40d7d903a4e8585e2349fdff7613
--- /dev/null
+++ b/client/src/app/app-init.ts
@@ -0,0 +1,29 @@
+import { APP_INITIALIZER } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+
+import { KeycloakService } from 'keycloak-angular';
+import { Store } from '@ngrx/store';
+
+import { AppConfigService } from './app-config.service';
+import { initializeKeycloak } from 'src/app/auth/init.keycloak';
+
+function appInit(http: HttpClient, appConfigService: AppConfigService, keycloak: KeycloakService, store: Store<{ }>) {
+    return () => {
+        return http.get('/assets/app.config.json')
+            .toPromise()
+            .then(data => {
+                Object.assign(appConfigService, data);
+                return appConfigService;
+            })
+            .then(appConfigService => {
+                return initializeKeycloak(keycloak, store, appConfigService)
+            });
+    }
+}
+
+export const appInitializer = {
+    provide: APP_INITIALIZER,
+    useFactory: appInit,
+    multi: true,
+    deps: [ HttpClient, AppConfigService, KeycloakService, Store ]
+};
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts
index 8c1885fa65c4ae6e4b4f783cb4081092fb62f45d..a003f5b07cd14c37cb0b2e4f3af9be40d224461f 100644
--- a/client/src/app/app-routing.module.ts
+++ b/client/src/app/app-routing.module.ts
@@ -11,9 +11,14 @@ import { NgModule } from '@angular/core';
 import { RouterModule, Routes } from '@angular/router';
 
 import { NotFoundPageComponent } from './core/containers/not-found-page.component';
+import { UnauthorizedComponent } from './core/containers/unauthorized.component';
 
 const routes: Routes = [
-    { path: '', redirectTo: 'portal-home', pathMatch: 'full' },
+    { path: '', redirectTo: 'portal', pathMatch: 'full' },
+    { path: 'portal', loadChildren: () => import('./portal/portal.module').then(m => m.PortalModule) },
+    { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) },
+    { path: 'instance', loadChildren: () => import('./instance/instance.module').then(m => m.InstanceModule) },
+    { path: 'unauthorized', component: UnauthorizedComponent },
     { path: '**', component: NotFoundPageComponent }
 ];
 
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index 8914bdcb34ef2a4201d8378d55841bd2d75e3966..cca4aece56fc59af597c3db12456c7952748f33d 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -24,10 +24,9 @@ import { CustomSerializer } from './custom-route-serializer';
 import { CoreModule } from './core/core.module';
 import { AuthModule } from './auth/auth.module';
 import { MetamodelModule } from './metamodel/metamodel.module';
-import { PortalModule } from './portal/portal.module';
-import { AdminModule } from './admin/admin.module';
-import { InstanceModule } from './instance/instance.module';
 import { AppComponent } from './core/containers/app.component';
+import { AppConfigService } from './app-config.service';
+import { appInitializer } from './app-init';
 
 @NgModule({
     imports: [
@@ -37,9 +36,6 @@ import { AppComponent } from './core/containers/app.component';
         CoreModule,
         AuthModule,
         MetamodelModule,
-        PortalModule,
-        AdminModule,
-        InstanceModule,
         AppRoutingModule,
         StoreModule.forRoot(reducers, {
             metaReducers,
@@ -62,6 +58,10 @@ import { AppComponent } from './core/containers/app.component';
             logOnly: environment.production
         })
     ],
+    providers: [
+        AppConfigService,
+        appInitializer
+    ],
     bootstrap: [AppComponent]
 })
 export class AppModule { }
diff --git a/client/src/app/auth/auth.effects.ts b/client/src/app/auth/auth.effects.ts
index a98a959494731d20dfc60d4e4d09495359d6a64b..69cefb0e2b4f468564a02cdcbafb631a22113e7f 100644
--- a/client/src/app/auth/auth.effects.ts
+++ b/client/src/app/auth/auth.effects.ts
@@ -15,7 +15,7 @@ import { tap, switchMap } from 'rxjs/operators';
 import { KeycloakService } from 'keycloak-angular';
 
 import * as authActions from './auth.actions';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
  
 @Injectable()
 export class AuthEffects {
@@ -23,11 +23,7 @@ export class AuthEffects {
         this.actions$.pipe(
             ofType(authActions.login),
             tap(_ => {
-                let redirectUri = window.location.origin;
-                if (environment.baseHref !== '/') {
-                    redirectUri += environment.baseHref;
-                }
-                redirectUri += environment.ssoLoginRedirectUri;
+                let redirectUri = window.location.toString()
                 this.keycloak.login({ redirectUri });
             })    
         ),
@@ -39,10 +35,9 @@ export class AuthEffects {
             ofType(authActions.logout),
             tap(_ => {
                 let redirectUri = window.location.origin;
-                if (environment.baseHref !== '/') {
-                    redirectUri += environment.baseHref;
+                if (this.config.baseHref !== '/') {
+                    redirectUri += this.config.baseHref;
                 }
-                redirectUri += environment.ssoLogoutRedirectUri;
                 this.keycloak.logout(redirectUri);
             })        
         ),
@@ -66,13 +61,14 @@ export class AuthEffects {
     openEditProfile$ =  createEffect(() =>
         this.actions$.pipe(
             ofType(authActions.openEditProfile),
-            tap(_ => window.open(environment.ssoAuthUrl + '/realms/' + environment.ssoRealm + '/account', '_blank'))        
+            tap(_ => window.open(this.config.ssoAuthUrl + '/realms/' + this.config.ssoRealm + '/account', '_blank'))        
         ),
         { dispatch: false }
     );
  
     constructor(
         private actions$: Actions,
-        private keycloak: KeycloakService
+        private keycloak: KeycloakService,
+        private config: AppConfigService
     ) {}
 }
diff --git a/client/src/app/auth/auth.module.ts b/client/src/app/auth/auth.module.ts
index 1c04b526d5fa4866c40d04c2ecc6ccaae2284cfb..5f92830201a13e8f8b84300b2cab0cd4cd0989cb 100644
--- a/client/src/app/auth/auth.module.ts
+++ b/client/src/app/auth/auth.module.ts
@@ -13,19 +13,14 @@ import { KeycloakAngularModule } from 'keycloak-angular';
 import { StoreModule } from '@ngrx/store';
 import { EffectsModule } from '@ngrx/effects';
 
-import { initializeKeycloakAnis } from './init.keycloak';
 import { authReducer } from './auth.reducer';
 import { AuthEffects } from './auth.effects';
-import { environment } from 'src/environments/environment';
 
 @NgModule({
     imports: [
-        environment.authenticationEnabled ? KeycloakAngularModule : [],
+        KeycloakAngularModule,
         StoreModule.forFeature('auth', authReducer),
-        environment.authenticationEnabled ? EffectsModule.forFeature([ AuthEffects ]): []
-    ],
-    providers: [
-        environment.authenticationEnabled ? initializeKeycloakAnis: []
+        EffectsModule.forFeature([ AuthEffects ])
     ]
 })
 export class AuthModule { }
diff --git a/client/src/app/auth/init.keycloak.ts b/client/src/app/auth/init.keycloak.ts
index f0509a317a70c330bc5a31107aa51cc3c2f404cd..a5186253c05dd947ef3c8518fce6652ea60ced12 100644
--- a/client/src/app/auth/init.keycloak.ts
+++ b/client/src/app/auth/init.keycloak.ts
@@ -7,48 +7,41 @@
  * file that was distributed with this source code.
  */
 
-import { APP_INITIALIZER } from '@angular/core';
 import { from } from 'rxjs';
 
 import { KeycloakService, KeycloakEventType } from 'keycloak-angular';
 import { Store } from '@ngrx/store';
 
 import * as keycloakActions from './auth.actions';
-import * as fromKeycloak from './auth.reducer';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from '../app-config.service';
 
-function initializeKeycloak(keycloak: KeycloakService, store: Store<{ keycloak: fromKeycloak.State }>) {
-    return async () => {
-        from(keycloak.keycloakEvents$).subscribe(event => {
-            if (event.type === KeycloakEventType.OnAuthSuccess) {
-                store.dispatch(keycloakActions.authSuccess());
-            }
-        })
+export function initializeKeycloak(keycloak: KeycloakService, store: Store<{ }>, appConfigService: AppConfigService) {
+    if (!appConfigService.authenticationEnabled) {
+        return Promise.resolve(true);
+    }
 
-        let silentCheckSsoRedirectUri = window.location.origin;
-        if (environment.baseHref != '/') {
-            silentCheckSsoRedirectUri += environment.baseHref;
+    from(keycloak.keycloakEvents$).subscribe(event => {
+        if (event.type === KeycloakEventType.OnAuthSuccess) {
+            store.dispatch(keycloakActions.authSuccess());
         }
-        silentCheckSsoRedirectUri += '/assets/silent-check-sso.html';
+    })
 
-        return keycloak.init({
-            config: {
-                url: environment.ssoAuthUrl,
-                realm: environment.ssoRealm,
-                clientId: environment.ssoClientId,
-            },
-            initOptions: {
-                onLoad: 'check-sso',
-                silentCheckSsoRedirectUri
-            },
-            loadUserProfileAtStartUp: true
-        });
+    let silentCheckSsoRedirectUri = window.location.origin;
+    if (appConfigService.baseHref != '/') {
+        silentCheckSsoRedirectUri += appConfigService.baseHref;
     }
-}
+    silentCheckSsoRedirectUri += '/assets/silent-check-sso.html';
 
-export const initializeKeycloakAnis = {
-    provide: APP_INITIALIZER,
-    useFactory: initializeKeycloak,
-    multi: true,
-    deps: [ KeycloakService, Store ],
-};
+    return keycloak.init({
+        config: {
+            url: appConfigService.ssoAuthUrl,
+            realm: appConfigService.ssoRealm,
+            clientId: appConfigService.ssoClientId,
+        },
+        initOptions: {
+            onLoad: 'check-sso',
+            silentCheckSsoRedirectUri
+        },
+        loadUserProfileAtStartUp: true
+    });
+}
diff --git a/client/src/app/core/containers/app.component.spec.ts b/client/src/app/core/containers/app.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..49336f78e39111fb5200f7f60fc55ed08c608e96
--- /dev/null
+++ b/client/src/app/core/containers/app.component.spec.ts
@@ -0,0 +1,70 @@
+import { TestBed, waitForAsync, ComponentFixture  } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { of } from 'rxjs';
+import { provideMockStore, MockStore } from '@ngrx/store/testing';
+
+import { AppComponent } from './app.component';
+import { AppConfigService } from 'src/app/app-config.service';
+import * as authActions from 'src/app/auth/auth.actions';
+
+describe('AppComponent', () => {
+    let component: AppComponent;
+    let fixture: ComponentFixture<AppComponent>;
+    let store: MockStore;
+    let config: AppConfigService
+
+    beforeEach(waitForAsync(() => {
+        TestBed.configureTestingModule({
+            imports: [RouterTestingModule],
+            declarations: [
+                AppComponent
+            ],
+            providers: [
+                provideMockStore({ }),
+                AppConfigService
+            ]
+        }).compileComponents();
+        fixture = TestBed.createComponent(AppComponent);
+        component = fixture.componentInstance;
+        store = TestBed.inject(MockStore);
+        config = TestBed.inject(AppConfigService);
+    }));
+
+    it('should create the app', () => {
+        expect(component).toBeDefined();
+    });
+
+    it('authenticationEnabled() should give authentication enabled config key value', () => {
+        config.authenticationEnabled = true;
+        expect(component.authenticationEnabled()).toBeTruthy();
+    });
+
+    it('login() should dispatch login action', () => {
+        const spy = jest.spyOn(store, 'dispatch');
+        component.login();
+        expect(spy).toHaveBeenCalledTimes(1);
+        expect(spy).toHaveBeenCalledWith(authActions.login());
+    });
+
+    it('logout() should dispatch logout action', () => {
+        const spy = jest.spyOn(store, 'dispatch');
+        component.logout();
+        expect(spy).toHaveBeenCalledTimes(1);
+        expect(spy).toHaveBeenCalledWith(authActions.logout());
+    });
+
+    it('openEditProfile() should dispatch open edit profile action', () => {
+        const spy = jest.spyOn(store, 'dispatch');
+        component.openEditProfile();
+        expect(spy).toHaveBeenCalledTimes(1);
+        expect(spy).toHaveBeenCalledWith(authActions.openEditProfile());
+    });
+
+    it('isAnisAdmin() should return observable true if user is authenticated', () => {
+        component.userRoles = of(['user']);
+        component.isAnisAdmin().subscribe(isAuthenticated => expect(isAuthenticated).toBeFalsy());
+        component.userRoles = of(['user', 'anis_admin']);
+        component.isAnisAdmin().subscribe(isAuthenticated => expect(isAuthenticated).toBeTruthy());
+    });
+});
diff --git a/client/src/app/core/containers/app.component.ts b/client/src/app/core/containers/app.component.ts
index c265196bb2531b78a0262cc364ced47b63bd68f7..7ede1446430a597d5ef318ba39940265e28aa3e0 100644
--- a/client/src/app/core/containers/app.component.ts
+++ b/client/src/app/core/containers/app.component.ts
@@ -17,7 +17,7 @@ import * as fromAuth from '../../auth/auth.reducer';
 import * as authActions from '../../auth/auth.actions';
 import * as authSelector from '../../auth/auth.selector';
 import { UserProfile } from '../../auth/user-profile.model';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from '../../app-config.service';
 
 @Component({
   selector: 'app-root',
@@ -30,14 +30,14 @@ export class AppComponent {
     public userProfile: Observable<UserProfile>;
     public userRoles: Observable<string[]>;
 
-    constructor(private store: Store<{ auth: fromAuth.State }>) {
+    constructor(private store: Store<{ auth: fromAuth.State }>, private config: AppConfigService) {
         this.isAuthenticated = store.select(authSelector.selectIsAuthenticated);
         this.userProfile = store.select(authSelector.selectUserProfile);
         this.userRoles = store.select(authSelector.selectUserRoles);
     }
 
     authenticationEnabled(): boolean {
-        return environment.authenticationEnabled;
+        return this.config.authenticationEnabled;
     }
 
     login(): void {
diff --git a/client/src/app/core/containers/not-found-page.component.spec.ts b/client/src/app/core/containers/not-found-page.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..96dabccdf7c2923c7fd8b31e450f5b092d6addaa
--- /dev/null
+++ b/client/src/app/core/containers/not-found-page.component.spec.ts
@@ -0,0 +1,24 @@
+import { TestBed, waitForAsync, ComponentFixture  } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NotFoundPageComponent } from './not-found-page.component';
+
+describe('NotFoundPageComponent', () => {
+    let component: NotFoundPageComponent;
+    let fixture: ComponentFixture<NotFoundPageComponent>;
+
+    beforeEach(waitForAsync(() => {
+        TestBed.configureTestingModule({
+            imports: [RouterTestingModule],
+            declarations: [
+                NotFoundPageComponent
+            ]
+        }).compileComponents();
+        fixture = TestBed.createComponent(NotFoundPageComponent);
+        component = fixture.componentInstance;
+    }));
+
+    it('should create the not found page component', () => {
+        expect(component).toBeDefined();
+    });
+});
diff --git a/client/src/app/core/containers/unauthorized.component.html b/client/src/app/core/containers/unauthorized.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..becf4b8eb909051595b0a3a2e989989a8c6558f0
--- /dev/null
+++ b/client/src/app/core/containers/unauthorized.component.html
@@ -0,0 +1,12 @@
+<main role="main" class="container-fluid pb-4">
+    <div class="container">
+        <div class="text-center">
+            <img class="mb-4" src="assets/cesam_anis80.png" alt="">
+        
+            <p>
+                You are not authorized to navigate to this interface (403).<br />
+                Please contact the administrator to increase your access rights.
+            </p>
+        </div>
+    </div>
+</main>
diff --git a/client/src/app/core/containers/unauthorized.component.spec.ts b/client/src/app/core/containers/unauthorized.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..51a3acc62289172bc5dbbf79227cb3b40da9af16
--- /dev/null
+++ b/client/src/app/core/containers/unauthorized.component.spec.ts
@@ -0,0 +1,24 @@
+import { TestBed, waitForAsync, ComponentFixture  } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { UnauthorizedComponent } from './unauthorized.component';
+
+describe('UnauthorizedComponent', () => {
+    let component: UnauthorizedComponent;
+    let fixture: ComponentFixture<UnauthorizedComponent>;
+
+    beforeEach(waitForAsync(() => {
+        TestBed.configureTestingModule({
+            imports: [RouterTestingModule],
+            declarations: [
+                UnauthorizedComponent
+            ]
+        }).compileComponents();
+        fixture = TestBed.createComponent(UnauthorizedComponent);
+        component = fixture.componentInstance;
+    }));
+
+    it('should create the unauthorized component', () => {
+        expect(component).toBeDefined();
+    });
+});
diff --git a/client/src/app/core/containers/unauthorized.component.ts b/client/src/app/core/containers/unauthorized.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6567df192b3a61a8eb72d622f9efa38193459d22
--- /dev/null
+++ b/client/src/app/core/containers/unauthorized.component.ts
@@ -0,0 +1,7 @@
+import { Component } from '@angular/core';
+
+@Component({
+    selector: 'app-unauthorized',
+    templateUrl: 'unauthorized.component.html'
+})
+export class UnauthorizedComponent { }
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index 76f5e2493172efaca784d12955e136c857ca7b82..45581a8c53bd35ba20747f1e8fe72d2bfdacb1df 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -13,6 +13,7 @@ import { RouterModule } from '@angular/router';
 
 import { AppComponent } from './containers/app.component';
 import { NotFoundPageComponent } from './containers/not-found-page.component';
+import { ToastrModule } from 'ngx-toastr';
 
 export const COMPONENTS = [
     AppComponent,
@@ -22,7 +23,8 @@ export const COMPONENTS = [
 @NgModule({
     imports: [
         CommonModule,
-        RouterModule
+        RouterModule,
+        ToastrModule.forRoot()
     ],
     declarations: COMPONENTS,
     exports: COMPONENTS
diff --git a/client/src/app/instance/detail/components/default/default-object.component.html b/client/src/app/instance/detail/components/default/default-object.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..bc6843ae7d94023d1cd8b39b9c9c57631d17822f
--- /dev/null
+++ b/client/src/app/instance/detail/components/default/default-object.component.html
@@ -0,0 +1,11 @@
+<div class="row justify-content-center">
+    <div class="col col-lg-10 col-xl-8 mt-4">
+        <app-object-data
+            [datasetSelected]="datasetSelected"
+            [outputFamilyList]="outputFamilyList"
+            [outputCategoryList]="outputCategoryList"
+            [attributeList]="attributeList"
+            [object]="object">
+        </app-object-data>
+    </div>
+</div>
diff --git a/client/src/app/instance/detail/components/default/default-object.component.ts b/client/src/app/instance/detail/components/default/default-object.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..058a6eda84682117c638f3b96313f04f6ae192bb
--- /dev/null
+++ b/client/src/app/instance/detail/components/default/default-object.component.ts
@@ -0,0 +1,29 @@
+/**
+ * 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, Input, ChangeDetectionStrategy } from '@angular/core';
+
+import { Attribute, OutputFamily, OutputCategory } from 'src/app/metamodel/models';
+
+@Component({
+    selector: 'app-default-object',
+    templateUrl: 'default-object.component.html',
+    changeDetection: ChangeDetectionStrategy.OnPush
+})
+/**
+ * @class
+ * @classdesc Detail default object component.
+ */
+export class DefaultObjectComponent {
+    @Input() datasetSelected: string;
+    @Input() outputFamilyList: OutputFamily[];
+    @Input() outputCategoryList: OutputCategory[];
+    @Input() attributeList: Attribute[];
+    @Input() object: any;
+}
diff --git a/client/src/app/instance/detail/components/default/index.ts b/client/src/app/instance/detail/components/default/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f36eed1181aaad1346a0f96e9b5375b1f68979b2
--- /dev/null
+++ b/client/src/app/instance/detail/components/default/index.ts
@@ -0,0 +1,5 @@
+import { DefaultObjectComponent } from './default-object.component';
+
+export const defaultComponents = [
+    DefaultObjectComponent
+];
diff --git a/client/src/app/instance/detail/components/index.ts b/client/src/app/instance/detail/components/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e0d32c36208a417c4ec089814d619e25f5a69c6b
--- /dev/null
+++ b/client/src/app/instance/detail/components/index.ts
@@ -0,0 +1,9 @@
+import { defaultComponents } from './default';
+import { spectraComponents } from './spectra';
+import { ObjectDataComponent } from './object-data.component';
+
+export const dummiesComponents = [
+    defaultComponents,
+    spectraComponents,
+    ObjectDataComponent
+];
diff --git a/client/src/app/instance/detail/components/object-data.component.html b/client/src/app/instance/detail/components/object-data.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..8c469250b9ef97abbc24a6c81a407076bef0fa86
--- /dev/null
+++ b/client/src/app/instance/detail/components/object-data.component.html
@@ -0,0 +1,66 @@
+<div *ngIf="getAttributeBySearchFlag('RA') && getAttributeBySearchFlag('DEC')" class="row">
+    <div class="col-12">
+        <table class="table mb-1">
+            <tr>
+                <th>Alpha</th>
+                <th>Delta</th>
+                <th class="text-center" rowspan="2"><img src="assets/cesam_anis80.png" alt="CeSAM logo" /></th>
+            </tr>
+            <tr>
+                <td>{{ object[getAttributeBySearchFlag('RA').label] }}</td>
+                <td>{{ object[getAttributeBySearchFlag('DEC').label] }}</td>
+            </tr>
+        </table>
+        <hr class="mt-0 mb-4">
+    </div>
+</div>
+
+<!-- Accordion families -->
+<accordion [isAnimated]="true">
+    <accordion-group *ngFor="let family of outputFamilyList" #ag [isOpen]="true" class="pl-2">
+        <button class="btn btn-link btn-block clearfix pb-2" accordion-heading>
+            <span class="pull-left float-left text-primary">
+                {{ family.label }}
+                &nbsp;
+                <span *ngIf="ag.isOpen">
+                            <span class="fas fa-chevron-up"></span>
+                        </span>
+                <span *ngIf="!ag.isOpen">
+                            <span class="fas fa-chevron-down"></span>
+                        </span>
+            </span>
+        </button>
+
+        <!-- Accordion categories -->
+        <accordion [isAnimated]="true">
+            <accordion-group *ngFor="let category of getCategoryByFamilySortedByDisplay(family.id)" #ag [isOpen]="true" class="pl-4">
+                <button class="btn btn-link btn-block clearfix pb-2" accordion-heading>
+                    <span class="pull-left float-left text-primary">
+                        {{ category.label }}
+                        &nbsp;
+                        <span *ngIf="ag.isOpen">
+                            <span class="fas fa-chevron-up"></span>
+                        </span>
+                        <span *ngIf="!ag.isOpen">
+                            <span class="fas fa-chevron-down"></span>
+                        </span>
+                    </span>
+                </button>
+
+                <!-- Output list -->
+                <div *ngFor="let attribute of getAttributesVisibleByCategory(category.id)" class="row pb-2">
+                    <div class="col-5 font-weight-bold">{{ attribute.form_label }}</div>
+                    <ng-container [ngSwitch]="attribute.renderer_detail">
+                        <div *ngSwitchCase="'download'" class="col">
+                            <a [href]="getDownloadHref(object[attribute.label])" role="button" class="btn btn-primary btn-sm">
+                                <span class="fas fa-download"></span>
+                                {{ object[attribute.label] }}
+                            </a>
+                        </div>
+                        <div *ngSwitchDefault class="col">{{ object[attribute.label] }}</div>
+                    </ng-container >
+                </div>
+            </accordion-group>
+        </accordion>
+    </accordion-group>
+</accordion>
diff --git a/client/src/app/instance/detail/components/object-data.component.ts b/client/src/app/instance/detail/components/object-data.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..16de054ffd8348a0701382356e74f9412523540e
--- /dev/null
+++ b/client/src/app/instance/detail/components/object-data.component.ts
@@ -0,0 +1,86 @@
+/**
+ * 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, Input, ChangeDetectionStrategy } from '@angular/core';
+
+import { Attribute, OutputCategory, OutputFamily } from 'src/app/metamodel/models';
+import { getHost } from 'src/app/shared/utils';
+import { AppConfigService } from 'src/app/app-config.service';
+
+@Component({
+    selector: 'app-object-data',
+    templateUrl: 'object-data.component.html',
+    changeDetection: ChangeDetectionStrategy.OnPush
+})
+/**
+ * @class
+ * @classdesc Detail object data component.
+ */
+export class ObjectDataComponent {
+    @Input() datasetSelected: string;
+    @Input() outputFamilyList: OutputFamily[];
+    @Input() outputCategoryList: OutputCategory[];
+    @Input() attributeList: Attribute[];
+    @Input() object: any;
+
+    constructor(private appConfig: AppConfigService) { }
+
+    /**
+     * Returns category list sorted by display, for the given output family ID.
+     *
+     * @param  {number} idFamily - The output family ID.
+     *
+     * @return Category[]
+     */
+    getCategoryByFamilySortedByDisplay(idFamily: number): OutputCategory[] {
+        return this.outputCategoryList
+            .filter(category => category.id_output_family === idFamily)
+            //.sort(sortByDisplay);
+    }
+
+    /**
+     * Returns attribute list sorted by detail display, for the given output category ID.
+     *
+     * @param  {number} idCategory - The output category ID.
+     *
+     * @return Attribute[]
+     */
+    getAttributesVisibleByCategory(idCategory: number): Attribute[] {
+        return this.attributeList
+            .filter(a => a.detail)
+            .filter(a => a.id_output_category === idCategory)
+            .sort((a, b) => a.display_detail - b.display_detail);
+    }
+
+    /**
+     * Returns attribute list sorted by detail display.
+     *
+     * @return Attribute[]
+     */
+    getAttributesVisible(): Attribute[] {
+        return this.attributeList
+            .filter(a => a.detail)
+            .sort((a, b) => a.display_detail - b.display_detail);
+    }
+
+    /**
+     * Returns attribute for the given search flag.
+     *
+     * @param  {string} searchFlag - The search flag.
+     *
+     * @return Attribute
+     */
+    getAttributeBySearchFlag(searchFlag: string): Attribute {
+        return this.getAttributesVisible().find(attribute => attribute.search_flag === searchFlag);
+    }
+
+    getDownloadHref(value: string) {
+        return getHost(this.appConfig.apiUrl) + '/download-file/' + this.datasetSelected + '/' + value;
+    }
+}
diff --git a/client/src/app/instance/detail/components/spectra/graph/point.ts b/client/src/app/instance/detail/components/spectra/graph/point.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b45ba9d36139645b8cbb14a7bf558423d614a976
--- /dev/null
+++ b/client/src/app/instance/detail/components/spectra/graph/point.ts
@@ -0,0 +1,13 @@
+/**
+ * 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.
+ */
+
+export interface Point {
+    x: number,
+    y: number
+}
diff --git a/client/src/app/instance/detail/components/spectra/graph/rays.ts b/client/src/app/instance/detail/components/spectra/graph/rays.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8dbba2bc26073ec94082ec9804d58755aa6beb17
--- /dev/null
+++ b/client/src/app/instance/detail/components/spectra/graph/rays.ts
@@ -0,0 +1,108 @@
+/**
+ * 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.
+ */
+
+ export const emissionLines = [
+    {name: 'SII', wavelength: 10320},
+    {name: 'HeI', wavelength: 7065.2},
+    {name: 'SII', wavelength: 6732.68},
+    {name: 'SII', wavelength: 6718.39},
+    {name: 'NII', wavelength: 6585.27},
+    {name: 'Hα', wavelength: 6564.61},
+    {name: 'NII', wavelength: 6549.86},
+    {name: 'HeI', wavelength: 5876.0},
+    {name: 'OIII', wavelength: 5008.24},
+    {name: 'OIII', wavelength: 4960.29},
+    {name: 'Hβ', wavelength: 4862.70},
+    {name: 'OIII', wavelength: 4364.44},
+    {name: 'Hγ', wavelength: 4341.68},
+    {name: 'Hδ', wavelength: 4102.89},
+    {name: 'SII', wavelength: 4072.0},
+    {name: 'Hksi', wavelength: 3890.15},
+    {name: 'NeIII', wavelength: 3869.81},
+    {name: 'H9', wavelength: 3836.47},
+    {name: 'Hθ', wavelength: 3798.98},
+    {name: 'H11', wavelength: 3771.70},
+    {name: 'OII', wavelength: 3728.49},
+    {name: 'NeV', wavelength: 3426.5},
+    {name: 'NeV', wavelength: 3346.4},
+    {name: 'HeII', wavelength: 3204.03},
+    {name: 'FeII', wavelength: 2964.0},
+    {name: 'MgII', wavelength: 2799.0},
+    {name: 'NII', wavelength: 2142.0},
+    {name: 'CIII', wavelength: 1908.73},
+    {name: 'HeII', wavelength: 1640.42},
+    {name: 'CIV', wavelength: 1549.0},
+    {name: 'SiIV', wavelength: 1397.0},
+    {name: 'CII', wavelength: 1334.53},
+    {name: 'OI', wavelength: 1303.0},
+    {name: 'NV', wavelength: 1240.0},
+    {name: 'LyA', wavelength: 1215.67},
+    {name: 'OVI', wavelength: 1033.0},
+    {name: 'LyB', wavelength: 1025.72},
+    {name: 'LyG', wavelength: 972.53}
+];
+
+export const absorptionLines = [
+    {name: 'TiO', wavelength: 8863.0},
+    {name: 'TiO', wavelength: 8430.0},
+    {name: 'NaI', wavelength: 8197.05},
+    {name: 'NaI', wavelength: 8185.50},
+    {name: 'TiO', wavelength: 7590.0},
+    {name: 'HeI', wavelength: 7065.2},
+    {name: 'Hα', wavelength: 6564.61},
+    {name: 'TiO', wavelength: 6159.0},
+    {name: 'NaD', wavelength: 5892.5},
+    {name: 'TiO', wavelength: 5603.0},
+    {name: 'Ca,Fe', wavelength: 5269.0},
+    {name: 'MgI', wavelength: 5174.12},
+    {name: 'Hβ', wavelength: 4862.70},
+    {name: 'Hγ', wavelength: 4341.68},
+    {name: 'Gband', wavelength: 4304.4},
+    {name: 'CN', wavelength: 4216.0},
+    {name: 'Hδ', wavelength: 4102.89},
+    {name: 'CaII_H', wavelength: 3969.59},
+    {name: 'CaII_K', wavelength: 3934.78},
+    {name: 'Hksi', wavelength: 3890.15},
+    {name: 'H9', wavelength: 3836.47},
+    {name: 'Hθ', wavelength: 3798.98},
+    {name: 'H11', wavelength: 3771.70},
+    {name: 'FeI', wavelength: 3581.0},
+    {name: 'HeII', wavelength: 3204.03},
+    {name: 'FeII', wavelength: 2964.0},
+    {name: 'MgII', wavelength: 2796.35},
+    {name: 'MgII', wavelength: 2803.53},
+    {name: 'FeII', wavelength: 2626.45},
+    {name: 'FeII', wavelength: 2600.17},
+    {name: 'FeII', wavelength: 2586.65},
+    {name: 'FeII', wavelength: 2382.76},
+    {name: 'FeII', wavelength: 2374.46},
+    {name: 'FeII', wavelength: 2344.21},
+    {name: 'FeII', wavelength: 2260.78},
+    {name: 'AlIII', wavelength: 1854.72},
+    {name: 'AlII', wavelength: 1670.78},
+    {name: 'HeII', wavelength: 1640.42},
+    {name: 'FeII', wavelength: 1608.45},
+    {name: 'CIV', wavelength: 1548.20},
+    {name: 'CIV', wavelength: 1550.77},
+    {name: 'SiII', wavelength: 1533.43},
+    {name: 'SiII', wavelength: 1526.71},
+    {name: 'SiIV', wavelength: 1402.77},
+    {name: 'SiIV', wavelength: 1393.75},
+    {name: 'CII', wavelength: 1334.53},
+    {name: 'OI', wavelength: 1302.17},
+    {name: 'OI', wavelength: 1304.86},
+    {name: 'SiII', wavelength: 1260.42},
+    {name: 'NV', wavelength: 1238.82},
+    {name: 'NV', wavelength: 1242.80},
+    {name: 'LyA', wavelength: 1215.67},
+    {name: 'OVI', wavelength: 1037.62},
+    {name: 'OVI', wavelength: 1031.93},
+    {name: 'LyB', wavelength: 1025.72},
+    {name: 'LyG', wavelength: 972.53}
+];
diff --git a/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.html b/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..9a1c8a0aeab69b32d8cd98a5a5b96e4f54aa65d6
--- /dev/null
+++ b/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.html
@@ -0,0 +1,3 @@
+<div id="svg-container">
+    <svg id="mygraph"></svg>
+</div>
diff --git a/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.scss b/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..5a58e12529fd174067354db0f9434c1a177a302a
--- /dev/null
+++ b/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.scss
@@ -0,0 +1,119 @@
+/**
+ * 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.
+ */
+
+.titles text {
+    fill: #5a5a5a;
+    font-size: 24px;
+}
+
+.axis line {
+    fill: none;
+    stroke: black;
+    shape-rendering: crispEdges;
+}
+
+.grid line {
+    fill: none;
+    stroke: lightgray;
+    shape-rendering: crispEdges;
+}
+
+.ray line {
+    stroke-dasharray: 3, 3;
+    stroke-width: 1.5px;
+}
+
+.emission line {
+    stroke: steelblue;
+}
+
+.emission text {
+    fill: steelblue;
+}
+
+.absorption line {
+    stroke: tomato;
+}
+
+.absorption text {
+    fill: tomato;
+}
+
+.spectra-line, .line {
+    fill: none;
+    stroke: black;
+    stroke-width: 1px;
+}
+
+.spectra-area, .area {
+    fill: #D0D0D0;
+    opacity: 0.6;
+}
+
+.overlay {
+    fill: none;
+    pointer-events: all;
+}
+
+.big-circle-tootlip {
+    fill: #7ac29a;
+}
+
+.little-circle-tooltip {
+    fill: rgb(16, 75, 42);
+    stroke: #fff;
+    stroke-width: 1.5px;
+}
+
+.rect-tootlip {
+    fill: #fafafa;
+    stroke: #7ac29a;
+    opacity: 0.9;
+    stroke-width: 1;
+}
+
+.text-tooltip {
+    font-size: 15px;
+    color: #333333;
+    fill: #333333;
+}
+
+.text-y-value {
+    font-weight: bold;
+}
+
+.emission-button circle {
+    stroke: steelblue;
+    stroke-width: 3px;
+    cursor: pointer;
+}
+
+.button-off circle {
+    fill: transparent;
+}
+
+.emission-button-on circle {
+    fill: steelblue;
+} 
+
+.absorption-button circle {
+    stroke: tomato;
+    stroke-width: 3px;
+    cursor: pointer;
+}
+
+.absorption-button-on circle {
+    fill: tomato;
+}
+
+.emission-button text, .absorption-button text {
+    font-size: 15px;
+    color: #333333;
+    fill: #333333;
+}
diff --git a/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.ts b/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c4be29c7a18d65b933ad6285702f7d809a642adb
--- /dev/null
+++ b/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.ts
@@ -0,0 +1,444 @@
+/**
+ * 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, ChangeDetectionStrategy, ViewEncapsulation, OnInit, Input } from '@angular/core';
+
+import * as d3 from 'd3';
+
+import { emissionLines, absorptionLines } from './rays';
+import { SpectraType } from './spectra-type';
+import { Point } from './point';
+
+@Component({
+    selector: 'app-spectra-graph',
+    encapsulation: ViewEncapsulation.None,
+    templateUrl: 'spectra-graph.component.html',
+    styleUrls: ['spectra-graph.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class SpectraGraphComponent implements OnInit {
+    @Input() z: number;
+    @Input() spectraCSV: string;
+
+    svg: d3.Selection<d3.BaseType, unknown, HTMLElement, any>;
+    focus: d3.Selection<SVGGElement, unknown, HTMLElement, any>;
+    width: number;
+    height: number;
+    brushHeight: number;
+    margin = { top: 50, right: 10, bottom: 150, left: 100 };
+    x: d3.ScaleLinear<number, number>;
+    xAxis: d3.Axis<number | { valueOf(): number }>;
+    y: d3.ScaleLinear<number, number>;
+    spectraLine: d3.Line<Point>;
+    spectraArea: d3.Area<Point>;
+    displayEmissionLines = true;
+    dispayAbsorptionLines = true;
+
+    ngOnInit() {
+        this.width = document.getElementById("svg-container").offsetWidth * 1 - this.margin.left - this.margin.right;
+        this.height = 600 - this.margin.top - this.margin.bottom;
+        this.brushHeight = 50;
+        this.x = d3.scaleLinear().range([0, this.width]);
+        this.y = d3.scaleLinear().range([this.height, 0]);
+
+        this.initSvg();
+        this.addTitles();
+
+        const dataset = d3.csvParse<Point, SpectraType>(this.spectraCSV, (row) => {
+            return {
+                x: parseFloat(row.x),
+                y: parseFloat(row.Flux)
+            };
+        });
+
+        this.setupDomain(dataset);
+        this.addGrid();
+        this.addSpectraLine(dataset);
+        this.addSpectraArea(dataset);
+        this.addRays();
+        this.addAxis();
+        this.addRaysButtons();
+        this.addBrush(dataset);
+        this.addTooltip(this.width, this.height, dataset, this.x, this.y);
+    }
+
+    private initSvg(): void {
+        this.svg = d3.select("svg#mygraph")
+            .attr("width", this.width + this.margin.left + this.margin.right)
+            .attr("height", this.height + this.margin.top + this.margin.bottom);
+
+        this.svg.append("defs").append("clipPath")
+            .attr("id", "clip")
+            .append("rect")
+            .attr("width", this.width)
+            .attr("height", this.height);
+
+        this.focus = this.svg.append("g")
+            .attr("class", "focus")
+            .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")");
+    }
+
+    private addTitles(): void {
+        const titles = this.focus.append("g")
+            .attr("class", "titles");
+
+        titles.append("text")
+            .attr("x", (this.width / 2))
+            .attr("y", this.height + this.margin.bottom - 20)
+            .attr("text-anchor", "middle")
+            .text("Wavelength [Ångströms]");
+
+        titles.append("text")
+            .attr("x", (this.margin.left * 0.75) * -1)
+            .attr("y", this.height / 2)
+            .attr("text-anchor", "middle")
+            .attr("transform", "rotate(-90," + ((this.margin.left * 0.75) * -1) + "," + this.height / 2 + ")")
+            .text("Flux Fλ (erg/sec/cm2/Å)");
+    }
+
+    private setupDomain(dataset: d3.DSVParsedArray<Point>): void {
+        const xMin = d3.min(dataset, (d) => d.x);
+        const xMax = d3.max(dataset, (d) => d.x);
+        this.x.domain([xMin, xMax]);
+
+        const yMin = d3.min(dataset, (d) => d.y);
+        const yMax = d3.max(dataset, (d) => d.y) * 1.1;
+        this.y.domain([yMin, yMax]);
+    }
+
+    private addGrid(): void {
+        const grid = this.focus.append("g")
+            .attr("class", "grid");
+
+        grid.append("g")
+            .attr("class", "grid-x")
+            .selectAll()
+            .data(this.x.ticks(10))
+            .enter()
+            .append("line")
+            .attr("class", "grid-line-x")
+            .attr("x1", (d: number) => this.x(d))
+            .attr("x2", (d: number) => this.x(d))
+            .attr("y1", 0)
+            .attr("y2", this.height);
+
+        grid.append("g")
+            .attr("class", "grid-y")
+            .selectAll()
+            .data(this.y.ticks(10))
+            .enter()
+            .append("line")
+            .attr("class", "grid-line-y")
+            .attr("x1", 0)
+            .attr("x2", this.width)
+            .attr("y1", (d: number) => this.y(d))
+            .attr("y2", (d: number) => this.y(d));
+    }
+
+    private addSpectraLine(dataset: d3.DSVParsedArray<Point>): void {
+        this.spectraLine = d3.line<Point>()
+            .x((d) => this.x(d.x))
+            .y((d) => this.y(d.y));
+
+        this.focus.append("g")
+            .attr("clip-path", "url(#clip)")
+            .append("path")
+            .datum(dataset)
+            .attr("class", "spectra-line")
+            .attr("d", this.spectraLine);
+    }
+
+    private addSpectraArea(dataset: d3.DSVParsedArray<Point>): void {
+        this.spectraArea = d3.area<Point>()
+            .x((d) => this.x(d.x))
+            .y0(this.height)
+            .y1((d) => this.y(d.y));
+
+        this.focus.append("g")
+            .attr("clip-path", "url(#clip)")
+            .append("path")
+            .datum(dataset)
+            .attr("class", "spectra-area")
+            .attr("d", this.spectraArea);
+    }
+
+    private addRays(): void {
+        const coef = 1 + this.z;
+
+        const er = this.focus.append("g")
+            .style("display", "block")
+            .attr("class", "ray emission")
+            .attr("clip-path", "url(#clip)");
+
+        er.selectAll()
+            .data(emissionLines)
+            .enter()
+            .append("line")
+            .attr("x1", r => this.x(r.wavelength * coef))
+            .attr("x2", r => this.x(r.wavelength * coef))
+            .attr("y1", this.height)
+            .attr("y2", 0);
+
+        er.selectAll()
+            .data(emissionLines)
+            .enter()
+            .append("text")
+            .attr("x", r => this.x(r.wavelength * coef) - 5)
+            .attr("y", this.height * 0.2)
+            .attr("transform", r => "rotate(-90, " + (this.x(r.wavelength * coef) - 5) + "," + this.height * 0.2 + ")")
+            .text(r => r.name);
+
+        const ar = this.focus.append("g")
+            .style("display", "block")
+            .attr("class", "ray absorption")
+            .attr("clip-path", "url(#clip)");
+
+        ar.selectAll()
+            .data(absorptionLines)
+            .enter()
+            .append("line")
+            .attr("x1", r => this.x(r.wavelength * coef))
+            .attr("x2", r => this.x(r.wavelength * coef))
+            .attr("y1", this.height)
+            .attr("y2", 0);
+
+        ar.selectAll()
+            .data(absorptionLines)
+            .enter()
+            .append("text")
+            .attr("x", r => this.x(r.wavelength * coef) - 5)
+            .attr("y", this.height * 0.8)
+            .attr("transform", r => "rotate(-90, " + (this.x(r.wavelength * coef) - 5) + "," + this.height * 0.8 + ")")
+            .text(r => r.name);
+    }
+
+    private addAxis(): void {
+        this.xAxis = d3.axisBottom(this.x);
+        this.focus.append("g")
+            .attr("class", "axis axis-x")
+            .attr("transform", "translate(0," + this.height + ")")
+            .call(this.xAxis);
+
+        this.focus.append("g")
+            .attr("class", "axis axis-y")
+            .call(d3.axisLeft(this.y).tickFormat(d3.format(".1e")))
+    }
+
+    private addRaysButtons(): void {
+        const gButtons = this.svg.append("g")
+            .attr("class", "rays-buttons")
+            .attr("transform", "translate(" + this.margin.left + "," + 25 + ")");
+
+        const emission = gButtons.append("g")
+            .attr("class", "emission-button emission-button-on");
+
+        const circleEmission = emission.append("circle")
+            .attr("cx", 20)
+            .attr("cy", 0)
+            .attr("r", 10);
+
+        circleEmission.on("click", () => {
+            const e = this.focus.select(".emission");
+            if (this.displayEmissionLines) {
+                e.style("display", "none");
+                emission.attr("class", "emission-button button-off");
+            } else {
+                e.style("display", "block");
+                emission.attr("class", "emission-button emission-button-on");
+            }
+            this.displayEmissionLines = !this.displayEmissionLines;
+        });
+
+        emission.append("text")
+            .attr("x", 35)
+            .attr("y", 5)
+            .text("Display emission lines")
+        
+        const absorption = gButtons.append("g")
+            .attr("class", "absorption-button absorption-button-on");
+
+        const circleAbsorption = absorption.append("circle")
+            .attr("cx", 215)
+            .attr("cy", 0)
+            .attr("r", 10);
+
+        circleAbsorption.on("click", () => {
+            const a = this.focus.select(".absorption");
+            if (this.dispayAbsorptionLines) {
+                a.style("display", "none");
+                absorption.attr("class", "absorption-button button-off");
+            } else {
+                a.style("display", "block");
+                absorption.attr("class", "absorption-button absorption-button-on");
+            }
+            this.dispayAbsorptionLines = !this.dispayAbsorptionLines;
+        });
+
+        absorption.append("text")
+            .attr("x", 230)
+            .attr("y", 5)
+            .text("Display absorption lines")
+
+    }
+
+    private addTooltip(
+        width: number,
+        height: number,
+        dataset: d3.DSVParsedArray<Point>,
+        x: d3.ScaleLinear<number, number>,
+        y: d3.ScaleLinear<number, number>
+    ): void {
+        const tooltip = this.focus.append("g")
+            .style("display", "none");
+        
+        tooltip.append("circle")
+            .attr("class", "big-circle-tootlip")
+            .attr("r", 10);
+
+        tooltip.append("circle")
+            .attr("class", "little-circle-tooltip")
+            .attr("r", 4);
+
+        tooltip.append("polyline")
+            .attr("points","0,0 0,40 55,40 60,45 65,40 135,40 135,0 0,0")
+            .attr("class", "rect-tootlip")
+            .attr("transform", "translate(-60, -55)");
+
+        const xValue = tooltip.append("text")
+            .attr("class", "text-tooltip")
+            .attr("transform", "translate(-55, -40)")
+            .append("tspan")
+            .text("X : ")
+            .append("tspan");
+        
+        const yValue = tooltip.append("text")
+            .attr("class", "text-tooltip")
+            .attr("transform", "translate(-55, -24)")
+            .append("tspan")
+            .text("Flux : ")
+            .append("tspan")
+            .attr("class", "text-y-value")
+            
+        const bisectX = d3.bisector((p: Point) => p.x).left;
+        this.focus.append("rect")
+            .attr("class", "overlay")
+            .attr("width", width)
+            .attr("height", height)
+            .on("mouseover", () => tooltip.style("display", null))
+            .on("mouseout", () => tooltip.style("display", "none"))
+            .on("mousemove", (d, i, n) => {
+                const node = n[i];
+                const mouse = d3.mouse(node);
+                const x0 = x.invert(mouse[0]);
+                const index = bisectX(dataset, x0);
+                const datum = dataset[index];
+                tooltip.attr("transform", "translate(" + x(datum.x) + "," + y(datum.y) + ")");
+                xValue.text(datum.x);
+                yValue.text(datum.y);
+            });
+    }
+
+    private addBrush(dataset: d3.DSVParsedArray<Point>): void {
+        const context = this.svg.append("g")
+            .attr("class", "context")
+            .attr("transform", "translate(" + this.margin.left + "," + 480 + ")");
+
+        const xBrush = d3.scaleLinear().range([0, this.width]);
+        const yBrush = d3.scaleLinear().range([this.brushHeight, 0]);
+        xBrush.domain(this.x.domain());
+        yBrush.domain(this.y.domain());
+
+        const xBrushAxis = d3.axisBottom(xBrush);
+
+        const lineBrush = d3.line<Point>()
+            .x((d) => xBrush(d.x))
+            .y((d) => yBrush(d.y));
+
+        context.append("g")
+            .attr("class", "line")
+            .append("path")
+            .datum(dataset)
+            .attr("d", lineBrush);
+
+        const areaBrush = d3.area<Point>()
+            .x((d) => xBrush(d.x))
+            .y0(this.brushHeight)
+            .y1((d) => yBrush(d.y));
+
+        context.append("g")
+            .attr("class", "area")
+            .append("path")
+            .datum(dataset)
+            .attr("d", areaBrush);
+
+        context.append("g")
+            .attr("class", "axis")
+            .attr("transform", "translate(0," + this.brushHeight + ")")
+            .call(xBrushAxis);
+
+        const brush = d3.brushX()
+            .extent([[0, 0], [this.width, this.brushHeight]])
+            .on("end", () => {
+                const selection = d3.event.selection || xBrush.range();
+                this.x.domain(selection.map(xBrush.invert, xBrush));
+
+                // Update spectra graph
+                this.focus.select(".spectra-line")
+                    .attr("d", this.spectraLine);
+                this.focus.select(".spectra-area")
+                    .attr("d", this.spectraArea);
+
+                // Update axis
+                this.focus.select(".axis-x").call(this.xAxis);
+
+                // Update grid
+                this.focus.selectAll(".grid-line-x")
+                    .remove();
+                this.focus.select(".grid-x")
+                    .selectAll()
+                    .data(this.x.ticks())
+                    .enter()
+                    .append("line")
+                    .attr("class", "grid-line-x")
+                    .attr("x1", (d: number) => this.x(d))
+                    .attr("x2", (d: number) => this.x(d))
+                    .attr("y1", 0)
+                    .attr("y2", this.height);
+
+                // Update rays
+                const coef = 1 + this.z;
+                this.focus.select(".emission")
+                    .selectAll("line")
+                    .data(emissionLines)
+                    .attr("x1", r => this.x(r.wavelength * coef))
+                    .attr("x2", r => this.x(r.wavelength * coef));
+                this.focus.select(".emission")
+                    .selectAll("text")
+                    .data(emissionLines)
+                    .attr("x", r => this.x(r.wavelength * coef) - 5)
+                    .attr("transform", r => "rotate(-90, " + (this.x(r.wavelength * coef) - 5) + "," + this.height * 0.2 + ")")
+
+                this.focus.select(".absorption")
+                    .selectAll("line")
+                    .data(absorptionLines)
+                    .attr("x1", r => this.x(r.wavelength * coef))
+                    .attr("x2", r => this.x(r.wavelength * coef));
+                this.focus.select(".absorption")
+                    .selectAll("text")
+                    .data(absorptionLines)
+                    .attr("x", r => this.x(r.wavelength * coef) - 5)
+                    .attr("transform", r => "rotate(-90, " + (this.x(r.wavelength * coef) - 5) + "," + this.height * 0.8 + ")")
+            });
+
+        context.append("g")
+            .attr("class", "brush")
+            .call(brush)
+            .call(brush.move, this.x.range());
+    }
+}
diff --git a/client/src/app/instance/detail/components/spectra/graph/spectra-type.ts b/client/src/app/instance/detail/components/spectra/graph/spectra-type.ts
new file mode 100644
index 0000000000000000000000000000000000000000..180320a2cb8bfb3061fc46289f3d2ee574464c53
--- /dev/null
+++ b/client/src/app/instance/detail/components/spectra/graph/spectra-type.ts
@@ -0,0 +1,10 @@
+/**
+ * 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.
+ */
+
+export type SpectraType = "x" | "Flux";
diff --git a/client/src/app/instance/detail/components/spectra/index.ts b/client/src/app/instance/detail/components/spectra/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..77b5e03935e6f4321da07852a1d56a460bc70c3a
--- /dev/null
+++ b/client/src/app/instance/detail/components/spectra/index.ts
@@ -0,0 +1,7 @@
+import { SpectraObjectComponent } from "./spectra-object.component";
+import { SpectraGraphComponent } from "./graph/spectra-graph.component";
+
+export const spectraComponents = [
+    SpectraObjectComponent,
+    SpectraGraphComponent
+];
diff --git a/client/src/app/instance/detail/components/spectra/spectra-object.component.html b/client/src/app/instance/detail/components/spectra/spectra-object.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..a5c472ccf3df9c49290d85d91f3e7890bf6df7ea
--- /dev/null
+++ b/client/src/app/instance/detail/components/spectra/spectra-object.component.html
@@ -0,0 +1,36 @@
+<div class="row">
+    <div *ngIf="getAttributeSpectraGraph()" class="col col-md-8 col-sm-12">
+        <div *ngIf="spectraIsLoading" id="div-spinner" class="text-center">
+            <span class="fas fa-circle-notch fa-spin fa-3x"></span>
+            <span class="sr-only">Loading...</span>
+        </div>
+        <app-spectra-graph *ngIf="spectraIsLoaded" [z]="getZ()" [spectraCSV]="spectraCSV"></app-spectra-graph>
+    </div>
+
+    <div [ngClass]="{'col-md-4 col-sm-12': getAttributeSpectraGraph()}" class="col mt-4">
+        <div *ngIf="getSpectra()" class="jumbotron row mb-3 p-4">
+            <div class="col-auto align-self-center">
+                <p>Download:</p>
+            </div>
+            <div class="w-100 d-block d-xl-none"></div>
+            <div class="col">
+                <div class="row justify-content-center">
+                    <div class="col-auto">
+                        <a [href]="getSpectra()" target="_blank" class="btn btn-lg btn-block dl-btn">
+                            <!-- <span [inlineSVG]="'assets/logo_spectra.svg'" title="Download SPECTRA archive"></span> -->
+                            Download SPECTRA archive
+                        </a>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <app-object-data
+            [datasetSelected]="datasetSelected"
+            [outputFamilyList]="outputFamilyList"
+            [outputCategoryList]="outputCategoryList"
+            [attributeList]="attributeList"
+            [object]="object">
+        </app-object-data>
+    </div>
+</div>
diff --git a/client/src/app/instance/detail/components/spectra/spectra-object.component.scss b/client/src/app/instance/detail/components/spectra/spectra-object.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..260a2036246abdb58c05276b1a262e50360ac3f3
--- /dev/null
+++ b/client/src/app/instance/detail/components/spectra/spectra-object.component.scss
@@ -0,0 +1,13 @@
+/**
+ * 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.
+ */
+
+.dl-btn {
+    height: 80px;
+    display: inline-block;
+}
diff --git a/client/src/app/instance/detail/components/spectra/spectra-object.component.ts b/client/src/app/instance/detail/components/spectra/spectra-object.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9c7095f2860c9712915d6dc5c61e0676fe7fc73d
--- /dev/null
+++ b/client/src/app/instance/detail/components/spectra/spectra-object.component.ts
@@ -0,0 +1,86 @@
+/**
+ * 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, Input, ChangeDetectionStrategy, Output, EventEmitter, OnInit } from '@angular/core';
+
+import { Attribute, OutputCategory, OutputFamily } from 'src/app/metamodel/models';
+import { getHost } from 'src/app/shared/utils';
+import { AppConfigService } from 'src/app/app-config.service';
+
+@Component({
+    selector: 'app-spectra-object',
+    templateUrl: 'spectra-object.component.html',
+    styleUrls: ['spectra-object.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush
+})
+/**
+ * @class
+ * @classdesc Detail spectra object component.
+ *
+ * @implements OnInit
+ */
+export class SpectraObjectComponent implements OnInit {
+    @Input() datasetSelected: string;
+    @Input() outputFamilyList: OutputFamily[];
+    @Input() outputCategoryList: OutputCategory[];
+    @Input() attributeList: Attribute[];
+    @Input() object: any;
+    @Input() spectraIsLoading: boolean;
+    @Input() spectraIsLoaded: boolean;
+    @Input() spectraCSV: string;
+    @Output() getSpectraCSV: EventEmitter<string> = new EventEmitter();
+
+    constructor(private appConfig: AppConfigService) { }
+
+    ngOnInit() {
+        const attributeSpectraGraph = this.getAttributeSpectraGraph();
+        if (attributeSpectraGraph) {
+            Promise.resolve(null).then(() => this.getSpectraCSV.emit(this.object[attributeSpectraGraph.label]));
+        }
+    }
+
+    /**
+     * Returns spectra file URL.
+     *
+     * @return string
+     */
+    getSpectra(): string {
+        const spectraAttribute: Attribute = this.attributeList
+            .filter(a => a.detail)
+            .find(attribute => attribute.search_flag === 'SPECTRUM_1D');
+        return getHost(this.appConfig.apiUrl) + '/download-file/' + this.datasetSelected + '/' + this.object[spectraAttribute.label];
+    }
+
+    /**
+     * Returns detail rendered spectra graph attribute.
+     *
+     * @return Attribute
+     */
+    getAttributeSpectraGraph(): Attribute {
+        return this.attributeList
+            .filter(a => a.detail)
+            .find(attribute => attribute.renderer_detail === 'spectra_graph');
+    }
+
+    /**
+     * Returns Z.
+     *
+     * @return number
+     */
+    getZ(): number {
+        const attributeZ = this.attributeList
+            .filter(a => a.detail)
+            .find(attribute => attribute.search_flag === 'Z');
+        if (attributeZ) {
+            return +this.object[attributeZ.label];
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/client/src/app/instance/detail/containers/detail.component.html b/client/src/app/instance/detail/containers/detail.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..2899b8cf78fc9f670a81366d3efd8b049e074bc6
--- /dev/null
+++ b/client/src/app/instance/detail/containers/detail.component.html
@@ -0,0 +1,34 @@
+<div class="container-fluid">
+    <div *ngIf="!(pristine | async)" class="row mt-2 mb-2 justify-content-between">
+        <div class="col">
+            <button (click)="goBackToResult()" class="btn btn-outline-secondary">
+                <span class="fa fa-backward"></span> Back to search results
+            </button>
+        </div>
+    </div>
+    <app-spinner *ngIf="(attributeListIsLoading | async)
+        || (outputFamilyListIsLoading | async)
+        || (outputCategoryListIsLoading | async)
+        || (objectIsLoading | async)">
+    </app-spinner>
+    <div *ngIf="(attributeListIsLoaded | async) && (outputFamilyListIsLoaded | async) && (outputCategoryListIsLoaded | async) && (objectIsLoaded | async)" [ngSwitch]="getObjectType(attributeList | async)">
+        <app-spectra-object *ngSwitchCase="'spectra'"
+            [datasetSelected]="datasetSelected | async"
+            [outputFamilyList]="outputFamilyList | async"
+            [outputCategoryList]="outputCategoryList | async"
+            [attributeList]="attributeList | async"
+            [object]="object | async"
+            [spectraCSV]="spectraCSV | async"
+            [spectraIsLoading]="spectraIsLoading | async"
+            [spectraIsLoaded]="spectraIsLoaded | async"
+            (getSpectraCSV)="getSpectraCSV($event)">
+        </app-spectra-object>
+        <app-default-object *ngSwitchDefault
+            [datasetSelected]="datasetSelected | async"
+            [outputFamilyList]="outputFamilyList | async"
+            [outputCategoryList]="outputCategoryList | async"
+            [attributeList]="attributeList | async"
+            [object]="object | async">
+        </app-default-object>
+    </div>
+</div>
diff --git a/client/src/app/instance/detail/containers/detail.component.ts b/client/src/app/instance/detail/containers/detail.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..331290c257df23a6736d98e8f7f702e3ab2da687
--- /dev/null
+++ b/client/src/app/instance/detail/containers/detail.component.ts
@@ -0,0 +1,143 @@
+/**
+ * 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, OnDestroy } from '@angular/core';
+import { Location } from '@angular/common';
+
+import { Store } from '@ngrx/store';
+import { Observable, Subscription } from 'rxjs';
+
+import { Attribute, OutputCategory, OutputFamily } from 'src/app/metamodel/models';
+import * as fromDetail from 'src/app/instance/store/reducers/detail.reducer';
+import * as detailActions from 'src/app/instance/store/actions/detail.actions';
+import * as detailSelector from 'src/app/instance/store/selectors/detail.selector';
+import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector';
+import * as searchSelector from 'src/app/instance/store/selectors/search.selector';
+import * as outputFamilyActions from 'src/app/metamodel/actions/output-family.actions';
+import * as outputFamilySelector from 'src/app/metamodel/selectors/output-family.selector';
+import * as outputCategoryActions from 'src/app/metamodel/actions/output-category.actions';
+import * as outputCategorySelector from 'src/app/metamodel/selectors/output-category.selector';
+import * as attributeActions from 'src/app/metamodel/actions/attribute.actions';
+import * as attributeSelector from 'src/app/metamodel/selectors/attribute.selector';
+
+/**
+ * Interface for store state.
+ *
+ * @interface StoreState
+ */
+interface StoreState {
+    detail: fromDetail.State;
+}
+
+@Component({
+    selector: 'app-detail-page',
+    templateUrl: 'detail.component.html'
+})
+/**
+ * @class
+ * @classdesc Detail container.
+ *
+ * @implements OnInit
+ * @implements OnDestroy
+ */
+export class DetailComponent implements OnInit, OnDestroy {
+    public datasetSelected: Observable<string>;
+    public pristine: Observable<boolean>;
+    public attributeList: Observable<Attribute[]>;
+    public attributeListIsLoading: Observable<boolean>;
+    public attributeListIsLoaded: Observable<boolean>;
+    public outputFamilyList: Observable<OutputFamily[]>;
+    public outputFamilyListIsLoading: Observable<boolean>;
+    public outputFamilyListIsLoaded: Observable<boolean>;
+    public outputCategoryList: Observable<OutputCategory[]>;
+    public outputCategoryListIsLoading: Observable<boolean>;
+    public outputCategoryListIsLoaded: Observable<boolean>;
+    public object: Observable<any>;
+    public objectIsLoading: Observable<boolean>;
+    public objectIsLoaded: Observable<boolean>;
+    public spectraCSV: Observable<string>;
+    public spectraIsLoading: Observable<boolean>;
+    public spectraIsLoaded: Observable<boolean>;
+
+    private attributeListIsLoadedSubscription: Subscription;
+
+    constructor(private location: Location, private store: Store<StoreState>) {
+        this.datasetSelected = store.select(datasetSelector.selectDatasetNameByRoute);
+        this.pristine = store.select(searchSelector.selectPristine);
+        this.attributeList = store.select(attributeSelector.selectAllAttributes);
+        this.attributeListIsLoading = store.select(attributeSelector.selectAttributeListIsLoading);
+        this.attributeListIsLoaded = store.select(attributeSelector.selectAttributeListIsLoaded);
+        this.outputFamilyList = store.select(outputFamilySelector.selectAllOutputFamilies);
+        this.outputFamilyListIsLoading = store.select(outputFamilySelector.selectOutputFamilyListIsLoading);
+        this.outputFamilyListIsLoaded = store.select(outputFamilySelector.selectOutputFamilyListIsLoaded);
+        this.outputCategoryList = store.select(outputCategorySelector.selectAllOutputCategories);
+        this.outputCategoryListIsLoading = store.select(outputCategorySelector.selectOutputCategoryListIsLoading);
+        this.outputCategoryListIsLoaded = store.select(outputCategorySelector.selectOutputCategoryListIsLoaded);
+        this.object = this.store.select(detailSelector.selectObject);
+        this.objectIsLoading = this.store.select(detailSelector.selectObjectIsLoading);
+        this.objectIsLoaded = this.store.select(detailSelector.selectObjectIsLoaded);
+        this.spectraCSV = this.store.select(detailSelector.selectSpectraCSV);
+        this.spectraIsLoading = this.store.select(detailSelector.selectObjectIsLoading);
+        this.spectraIsLoaded = this.store.select(detailSelector.selectSpectraIsLoaded);
+    }
+
+    ngOnInit() {
+        // Create a micro task that is processed after the current synchronous code
+        // This micro task prevent the expression has changed after view init error
+        Promise.resolve(null).then(() => this.store.dispatch(attributeActions.loadAttributeList()));
+        Promise.resolve(null).then(() => this.store.dispatch(outputFamilyActions.loadOutputFamilyList()));
+        Promise.resolve(null).then(() => this.store.dispatch(outputCategoryActions.loadOutputCategoryList()));
+        this.attributeListIsLoadedSubscription = this.attributeListIsLoaded.subscribe(attributeListIsLoaded => {
+            if (attributeListIsLoaded) {
+                Promise.resolve(null).then(() => this.store.dispatch(detailActions.retrieveObject()));
+            }
+        });
+    }
+
+    /**
+     * Returns the object type.
+     *
+     * @param  {Attribute[]} attributeList - The attribute list.
+     *
+     * @return string
+     */
+    getObjectType(attributeList: Attribute[]): string {
+        const spectrumAttribute: Attribute = attributeList
+            .filter(a => a.detail)
+            .find(attribute => attribute.search_flag === 'SPECTRUM_1D');
+        if (spectrumAttribute) {
+            return 'spectra';
+        }
+        return 'default';
+    }
+
+    /**
+     * Gets back to result page.
+     */
+    goBackToResult(): void {
+        this.location.back();
+    }
+
+    /**
+     * Dispatches action to retrieve spectra file.
+     *
+     * @param  {string} spectraFile - The spectra file name.
+     */
+    getSpectraCSV(spectraFile: string): void {
+        this.store.dispatch(detailActions.retrieveSpectra({ filename: spectraFile }));
+    }
+
+    /**
+     * Resets detail information.
+     */
+    ngOnDestroy() {
+        this.attributeListIsLoadedSubscription.unsubscribe();
+        // this.store.dispatch(new detailActions.DestroyDetailAction());
+    }
+}
diff --git a/client/src/app/instance/detail/detail-routing.module.ts b/client/src/app/instance/detail/detail-routing.module.ts
index ee1308922ba3f0b402a1a1bee41c26a5c08dce7e..8f88b99bb56b2ad21b228b818173bdb623ed7f6d 100644
--- a/client/src/app/instance/detail/detail-routing.module.ts
+++ b/client/src/app/instance/detail/detail-routing.module.ts
@@ -10,10 +10,11 @@
 import { NgModule } from '@angular/core';
 import { Routes, RouterModule } from '@angular/router';
 
-//import { DetailComponent } from './detail.component';
+import { DetailComponent } from './containers/detail.component';
 
 const routes: Routes = [
-    //{ path: '', component: DetailComponent }
+    { path: '', redirectTo: ':/dname/:id', pathMatch: 'full' },
+    { path: ':dname/:id', component: DetailComponent }
 ];
 
 @NgModule({
@@ -23,5 +24,5 @@ const routes: Routes = [
 export class DetailRoutingModule { }
 
 export const routedComponents = [
-    //DetailComponent
+    DetailComponent
 ];
diff --git a/client/src/app/instance/detail/detail.module.ts b/client/src/app/instance/detail/detail.module.ts
index 7e1605ceeda56e6d572a37d69b6fb11fb8e8bc7d..fbaaabfa3e090c12c51338d99b4ec7ebd6f32bdc 100644
--- a/client/src/app/instance/detail/detail.module.ts
+++ b/client/src/app/instance/detail/detail.module.ts
@@ -11,12 +11,16 @@ import { NgModule } from '@angular/core';
 
 import { SharedModule } from 'src/app/shared/shared.module';
 import { DetailRoutingModule, routedComponents } from './detail-routing.module';
+import { dummiesComponents } from './components';
 
 @NgModule({
     imports: [
         SharedModule,
         DetailRoutingModule
     ],
-    declarations: [routedComponents]
+    declarations: [
+        routedComponents,
+        dummiesComponents
+    ]
 })
 export class DetailModule { }
diff --git a/client/src/app/instance/documentation/containers/documentation.component.ts b/client/src/app/instance/documentation/containers/documentation.component.ts
index cd48d259a7848a8754ea007a8ade6ba1c658da9d..4146df4261f2d8bbc91ccaf3ca2309ea0be5dc27 100644
--- a/client/src/app/instance/documentation/containers/documentation.component.ts
+++ b/client/src/app/instance/documentation/containers/documentation.component.ts
@@ -14,8 +14,8 @@ import { Observable } from 'rxjs';
 
 import * as documentationActions from 'src/app/instance/store/actions/documentation.actions';
 import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector';
+import { AppConfigService } from 'src/app/app-config.service';
 import { Attribute } from 'src/app/metamodel/models';
-import { environment } from 'src/environments/environment';
 import * as instanceSelector from "../../../metamodel/selectors/instance.selector";
 import * as attributeSelector from "../../../metamodel/selectors/attribute.selector";
 
@@ -37,7 +37,7 @@ export class DocumentationComponent implements OnInit {
     public attributeListIsLoaded: Observable<boolean>;
     public attributeList: Observable<Attribute[]>;
 
-    constructor(private store: Store<{ }>) {
+    constructor(private store: Store<{ }>, private config: AppConfigService) {
         this.instanceSelected = store.select(instanceSelector.selectInstanceNameByRoute);
         this.datasetSelected = store.select(datasetSelector.selectDatasetNameByRoute);
         this.attributeListIsLoading = store.select(attributeSelector.selectAttributeListIsLoading);
@@ -55,10 +55,10 @@ export class DocumentationComponent implements OnInit {
      * @return string
      */
     getUrlServer(): string {
-        if (!environment.apiUrl.startsWith('http')) {
+        if (!this.config.apiUrl.startsWith('http')) {
             const url = window.location;
-            return url.protocol + '//' + url.host + environment.apiUrl;
+            return url.protocol + '//' + url.host + this.config.apiUrl;
         }
-        return environment.apiUrl;
+        return this.config.apiUrl;
     }
 }
diff --git a/client/src/app/instance/instance-routing.module.ts b/client/src/app/instance/instance-routing.module.ts
index 37baa1075d4b111e51c541b0cad73beb09937cb2..053f59c53dbca4657b86d991f96f63645148ad67 100644
--- a/client/src/app/instance/instance-routing.module.ts
+++ b/client/src/app/instance/instance-routing.module.ts
@@ -14,17 +14,18 @@ import { InstanceComponent } from './instance.component';
 
 const routes: Routes = [
     { 
-        path: 'instance/:iname', component: InstanceComponent, children: [
+        path: ':iname', component: InstanceComponent, children: [
             { path: '', redirectTo: 'home', pathMatch: 'full' },
             { path: 'home', loadChildren: () => import('./home/home.module').then(m => m.HomeModule) },
             { path: 'documentation', loadChildren: () => import('./documentation/documentation.module').then(m => m.DocumentationModule) },
-            { path: 'search', loadChildren: () => import('./search/search.module').then(m => m.SearchModule) }
+            { path: 'search', loadChildren: () => import('./search/search.module').then(m => m.SearchModule) },
+            { path: 'detail', loadChildren: () => import('./detail/detail.module').then(m => m.DetailModule) }
         ]
     }
 ];
 
 @NgModule({
-    imports: [RouterModule.forRoot(routes)],
+    imports: [RouterModule.forChild(routes)],
     exports: [RouterModule]
 })
 export class InstanceRoutingModule { }
diff --git a/client/src/app/instance/instance.component.html b/client/src/app/instance/instance.component.html
index 979df50e47db62138793be21dff9adf7df5e4cf1..61101934f1e33d3d27823eb8998cedcf423d891b 100644
--- a/client/src/app/instance/instance.component.html
+++ b/client/src/app/instance/instance.component.html
@@ -3,6 +3,8 @@
         [links]="links"
         [isAuthenticated]="isAuthenticated | async"
         [userProfile]="userProfile | async"
+        [baseHref]="getBaseHref()"
+        [authenticationEnabled]="getAuthenticationEnabled()"
         (login)="login()"
         (logout)="logout()"
         (openEditProfile)="openEditProfile()">
diff --git a/client/src/app/instance/instance.component.ts b/client/src/app/instance/instance.component.ts
index 15365842642b82d50d179a22cf95881fc4b13659..214fb769ab385501d5a877f016369ac40eb209f5 100644
--- a/client/src/app/instance/instance.component.ts
+++ b/client/src/app/instance/instance.component.ts
@@ -15,6 +15,7 @@ import { UserProfile } from 'src/app/auth/user-profile.model';
 import * as authActions from 'src/app/auth/auth.actions';
 import * as authSelector from 'src/app/auth/auth.selector';
 import * as metamodelActions from './store/actions/metamodel.actions';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Component({
     selector: 'app-instance',
@@ -35,7 +36,7 @@ export class InstanceComponent implements OnInit {
     public userProfile: Observable<UserProfile>;
     public userRoles: Observable<string[]>;
 
-    constructor(private store: Store<{ }>) {
+    constructor(private store: Store<{ }>, private config: AppConfigService) {
         this.isAuthenticated = store.select(authSelector.selectIsAuthenticated);
         this.userProfile = store.select(authSelector.selectUserProfile);
         this.userRoles = store.select(authSelector.selectUserRoles);
@@ -46,6 +47,14 @@ export class InstanceComponent implements OnInit {
         Promise.resolve(null).then(() => this.store.dispatch(metamodelActions.loadInstanceMetamodel()));
     }
 
+    getBaseHref() {
+        return this.config.baseHref;
+    }
+
+    getAuthenticationEnabled() {
+        return this.config.authenticationEnabled;
+    }
+
     login(): void {
         this.store.dispatch(authActions.login());
     }
diff --git a/client/src/app/instance/instance.reducer.ts b/client/src/app/instance/instance.reducer.ts
index 0c1981cf76c6306499f8c3167c22227f8dedc97f..f660ab9ab6c3dd59f99ddf24e047eac5cd2b82a7 100644
--- a/client/src/app/instance/instance.reducer.ts
+++ b/client/src/app/instance/instance.reducer.ts
@@ -13,17 +13,23 @@ import { RouterReducerState } from 'src/app/custom-route-serializer';
 import * as metamodel from './store/reducers/metamodel.reducer';
 import * as search from './store/reducers/search.reducer';
 import * as samp from './store/reducers/samp.reducer';
+import * as coneSearch from './store/reducers/cone-search.reducer';
+import * as detail from './store/reducers/detail.reducer';
 
 export interface State {
     metamodel: metamodel.State,
     search: search.State,
-    samp: samp.State
+    samp: samp.State,
+    coneSearch: coneSearch.State
+    detail: detail.State
 }
 
 const reducers = {
     metamodel: metamodel.metamodelReducer,
     search: search.searchReducer,
-    samp: samp.sampReducer
+    samp: samp.sampReducer,
+    coneSearch: coneSearch.coneSearchReducer,
+    detail: detail.detailReducer
 };
 
 export const instanceReducer = combineReducers(reducers);
diff --git a/client/src/app/instance/search/components/criteria/cone-search-tab.component.html b/client/src/app/instance/search/components/criteria/cone-search-tab.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..ea951c32bd3106f1c745ad5e18876c73d592bad5
--- /dev/null
+++ b/client/src/app/instance/search/components/criteria/cone-search-tab.component.html
@@ -0,0 +1,37 @@
+<accordion *ngIf="(datasetList | datasetByName:datasetSelected).config.cone_search.cone_search_enabled" [isAnimated]="true">
+    <accordion-group #ag [panelClass]="'custom-accordion'" [isOpen]="(datasetList | datasetByName:datasetSelected).config.cone_search.cone_search_opened" class="my-2">
+        <button class="btn btn-link btn-block clearfix" accordion-heading>
+            <span class="pull-left float-left">
+                Cone search
+                &nbsp;
+                <span *ngIf="ag.isOpen">
+                    <span class="fas fa-chevron-up"></span>
+                </span>
+                <span *ngIf="!ag.isOpen">
+                    <span class="fas fa-chevron-down"></span>
+                </span>
+            </span>
+        </button>
+        <div class="row">
+            <div class="col">
+                <app-cone-search
+                    [coneSearch]="coneSearch"
+                    [resolver]="resolver"
+                    [resolverIsLoading]="resolverIsLoading"
+                    [resolverIsLoaded]="resolverIsLoaded"
+                    (addConeSearch)="addConeSearch.emit($event)"
+                    (deleteConeSearch)="deleteConeSearch.emit()"
+                    (retrieveCoordinates)="retrieveCoordinates.emit($event)" #cs>
+                </app-cone-search>
+            </div>
+            <div class="col-2 text-center align-self-end">
+                <button class="btn btn-outline-success" *ngIf="!coneSearch" [hidden]="cs.form.invalid" (click)="addConeSearch.emit(cs.getConeSearch())">
+                    <span class="fas fa-plus fa-fw"></span>
+                </button>
+                <button class="btn btn-outline-danger" *ngIf="coneSearch" (click)="deleteConeSearch.emit()">
+                    <span class="fa fa-times fa-fw"></span>
+                </button>
+            </div>
+        </div>
+    </accordion-group>
+</accordion>
diff --git a/client/src/app/instance/search/components/criteria/cone-search-tab.component.ts b/client/src/app/instance/search/components/criteria/cone-search-tab.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..85a3f259e3df262a93c190d37c7733d31b1743dc
--- /dev/null
+++ b/client/src/app/instance/search/components/criteria/cone-search-tab.component.ts
@@ -0,0 +1,34 @@
+/**
+ * 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, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
+
+import { Dataset } from 'src/app/metamodel/models';
+import { ConeSearch, Resolver } from 'src/app/instance/store/models';
+
+@Component({
+    selector: 'app-cone-search-tab',
+    templateUrl: 'cone-search-tab.component.html',
+    changeDetection: ChangeDetectionStrategy.OnPush
+})
+/**
+ * @class
+ * @classdesc Search cone search tab component.
+ */
+export class ConeSearchTabComponent {
+    @Input() datasetSelected: string;
+    @Input() datasetList: Dataset[];
+    @Input() coneSearch: ConeSearch;
+    @Input() resolver: Resolver;
+    @Input() resolverIsLoading: boolean;
+    @Input() resolverIsLoaded: boolean;
+    @Output() addConeSearch: EventEmitter<ConeSearch> = new EventEmitter();
+    @Output() deleteConeSearch: EventEmitter<{ }> = new EventEmitter();
+    @Output() retrieveCoordinates: EventEmitter<string> = new EventEmitter();
+}
diff --git a/client/src/app/instance/search/components/criteria/index.ts b/client/src/app/instance/search/components/criteria/index.ts
index bdee82025471c6892a6af287afb3c151e019d2fd..88656ad3c2668f4eb770c6db2672fb8302115a68 100644
--- a/client/src/app/instance/search/components/criteria/index.ts
+++ b/client/src/app/instance/search/components/criteria/index.ts
@@ -1,8 +1,10 @@
+import { ConeSearchTabComponent } from './cone-search-tab.component';
 import { CriteriaTabsComponent } from './criteria-tabs.component';
 import { CriteriaByFamilyComponent } from './criteria-by-family.component';
 import { searchTypeComponents } from './search-type';
 
 export const criteriaComponents = [
+    ConeSearchTabComponent,
     CriteriaTabsComponent,
     CriteriaByFamilyComponent,
     searchTypeComponents
diff --git a/client/src/app/instance/search/components/criteria/search-type/between-date.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/between-date.component.spec.ts
deleted file mode 100644
index 72ebd410aa82ef81bf7c10fd618ab211ad3c581d..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/between-date.component.spec.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { Component, ViewChild } from '@angular/core';
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
-
-import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
-
-import { BetweenDateComponent } from './between-date.component';
-import { BetweenCriterion } from '../../../store/model';
-
-describe('[Search][Criteria][SearchType] Component: BetweenDateComponent', () => {
-    @Component({
-        selector: `app-host`,
-        template: `<app-between-date [criterion]='criterion'></app-between-date>`
-    })
-    class TestHostComponent {
-        @ViewChild(BetweenDateComponent, { static: false })
-        public testedComponent: BetweenDateComponent;
-        public criterion: BetweenCriterion = undefined;
-    }
-
-    let testHostComponent: TestHostComponent;
-    let testHostFixture: ComponentFixture<TestHostComponent>;
-    let testedComponent: BetweenDateComponent;
-
-    beforeEach(waitForAsync(() => {
-        TestBed.configureTestingModule({
-            declarations: [BetweenDateComponent, TestHostComponent],
-            imports: [BsDatepickerModule.forRoot(), FormsModule, ReactiveFormsModule]
-        });
-        testHostFixture = TestBed.createComponent(TestHostComponent);
-        testHostComponent = testHostFixture.componentInstance;
-        testHostFixture.detectChanges();
-        testedComponent = testHostComponent.testedComponent;
-    }));
-
-    it('should create the component', () => {
-        expect(testedComponent).toBeTruthy();
-    });
-
-    it('#getDateString() should format a datetime object into a string', () => {
-        const dateObject = new Date('February 08, 2019 15:47:00');
-        const expectedDateString = '2019-02-08';
-        const formatedDate = testedComponent.getDateString(dateObject);
-        expect(formatedDate).toEqual(expectedDateString);
-    });
-
-    it('#getDefault() should enable and not fill form as criterion not defined in host component', () => {
-        expect(testedComponent.field.value).toBeNull();
-        expect(testedComponent.field.enabled).toBeTruthy();
-    });
-
-    it('#getDefault() should fill and disable form if criterion is defined', () => {
-        const criterion = { id: 1, type: 'between', min: '2019-02-08', max: '2019-02-17' } as BetweenCriterion;
-        const expectedFieldValue = [new Date(criterion.min), new Date(criterion.max)];
-        testedComponent.getDefault(criterion);
-        expect(testedComponent.field.value).toEqual(expectedFieldValue);
-        expect(testedComponent.field.disabled).toBeTruthy();
-    });
-
-    it('raises the add criterion event when clicked', () => {
-        testedComponent.id = 1;
-        const dateMin = '2019-02-08';
-        const dateMax = '2019-02-17';
-        testedComponent.field = new FormControl([new Date(dateMin), new Date(dateMax)]);
-        const expectedCriterion = { id: testedComponent.id, type: 'between', min: dateMin, max: dateMax } as BetweenCriterion;
-        testedComponent.addCriterion.subscribe((event: BetweenCriterion) => expect(event).toEqual(expectedCriterion));
-        testedComponent.emitAdd();
-    });
-
-    it('raises the delete criterion event when clicked', () => {
-        testedComponent.id = 1;
-        testedComponent.deleteCriterion.subscribe((event: number) => expect(event).toEqual(1));
-        testedComponent.emitDelete();
-    });
-});
diff --git a/client/src/app/instance/search/components/criteria/search-type/between.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/between.component.spec.ts
deleted file mode 100644
index 80df0b7b01ffcf564995ab3e582e1cadc754b4b0..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/between.component.spec.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { Component, ViewChild } from '@angular/core';
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
-
-import { BetweenComponent } from './between.component';
-import { BetweenCriterion } from '../../../store/model';
-
-describe('[Search][Criteria][SearchType] Component: BetweenComponent', () => {
-    @Component({
-        selector: `app-host`,
-        template: `<app-between [criterion]='criterion'></app-between>`
-    })
-    class TestHostComponent {
-        @ViewChild(BetweenComponent, { static: false })
-        public testedComponent: BetweenComponent;
-        public criterion: BetweenCriterion = undefined;
-    }
-
-    let testHostComponent: TestHostComponent;
-    let testHostFixture: ComponentFixture<TestHostComponent>;
-    let testedComponent: BetweenComponent;
-
-    beforeEach(waitForAsync(() => {
-        TestBed.configureTestingModule({
-            declarations: [BetweenComponent, TestHostComponent],
-            imports: [FormsModule, ReactiveFormsModule]
-        });
-        testHostFixture = TestBed.createComponent(TestHostComponent);
-        testHostComponent = testHostFixture.componentInstance;
-        testHostFixture.detectChanges();
-        testedComponent = testHostComponent.testedComponent;
-    }));
-
-    it('should create the component', () => {
-        expect(testedComponent).toBeTruthy();
-    });
-
-    it('#getDefault() should enable and not fill form as criterion not defined in host component', () => {
-        expect(testedComponent.fieldMin.value).toBeNull();
-        expect(testedComponent.fieldMin.enabled).toBeTruthy();
-        expect(testedComponent.fieldMax.value).toBeNull();
-        expect(testedComponent.fieldMax.enabled).toBeTruthy();
-    });
-
-    it('#getDefault() should fill and disable form if criterion is defined', () => {
-        const min = '10';
-        const max = '20';
-        const criterion = { id: 1, type: 'between', min, max } as BetweenCriterion;
-        const expectedFieldMinValue = min;
-        const expectedFieldMaxValue = max;
-        testedComponent.getDefault(criterion);
-        expect(testedComponent.fieldMin.value).toEqual(expectedFieldMinValue);
-        expect(testedComponent.fieldMin.disabled).toBeTruthy();
-        expect(testedComponent.fieldMax.value).toEqual(expectedFieldMaxValue);
-        expect(testedComponent.fieldMax.disabled).toBeTruthy();
-    });
-
-    it('#getPlaceholderMin() should fill the placeholder if defined', () => {
-        const placeholder = '10';
-        testedComponent.placeholderMin = placeholder;
-        expect(testedComponent.getPlaceholderMin()).toEqual(placeholder);
-    });
-
-    it('#getPlaceholderMin() should not fill the placeholder if not defined', () => {
-        expect(testedComponent.getPlaceholderMin()).toEqual('');
-    });
-
-    it('#getPlaceholderMax() should fill the placeholder if defined', () => {
-        const placeholder = '10';
-        testedComponent.placeholderMax = placeholder;
-        expect(testedComponent.getPlaceholderMax()).toEqual(placeholder);
-    });
-
-    it('#getPlaceholderMax() should not fill the placeholder if not defined', () => {
-        expect(testedComponent.getPlaceholderMax()).toEqual('');
-    });
-
-    it('raises the add criterion event when clicked', () => {
-        testedComponent.id = 1;
-        const min = '10';
-        const max = '20';
-        testedComponent.fieldMin = new FormControl(min);
-        testedComponent.fieldMax = new FormControl(max);
-        const expectedCriterion = { id: testedComponent.id, type: 'between', min, max } as BetweenCriterion;
-        testedComponent.addCriterion.subscribe((event: BetweenCriterion) => expect(event).toEqual(expectedCriterion));
-        testedComponent.emitAdd();
-    });
-
-    it('raises the delete criterion event when clicked', () => {
-        testedComponent.id = 1;
-        testedComponent.deleteCriterion.subscribe((event: number) => expect(event).toEqual(1));
-        testedComponent.emitDelete();
-    });
-});
diff --git a/client/src/app/instance/search/components/criteria/search-type/checkbox.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/checkbox.component.spec.ts
deleted file mode 100644
index 0c10c5ec9edd0c2d1e345cc00cb52b6b65a4efe8..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/checkbox.component.spec.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { Component, ViewChild } from '@angular/core';
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { FormsModule, ReactiveFormsModule, FormControl, FormArray } from '@angular/forms';
-
-import { CheckboxComponent } from './checkbox.component';
-import { SelectMultipleCriterion } from '../../../store/model';
-import { Option } from '../../../../metamodel/model';
-
-describe('[Search][Criteria][SearchType] Component: CheckboxComponent', () => {
-    @Component({
-        selector: `app-host`,
-        template: `<app-checkbox [options]='options' [criterion]='criterion'></app-checkbox>`
-    })
-    class TestHostComponent {
-        @ViewChild(CheckboxComponent, { static: false })
-        public testedComponent: CheckboxComponent;
-        public criterion: SelectMultipleCriterion = undefined;
-        public options: Option[] = [
-            { label: 'One', value: 'one', display: 1 },
-            { label: 'Two', value: 'two', display: 2 },
-            { label: 'Three', value: 'three', display: 3 }
-        ];
-    }
-
-    let testHostComponent: TestHostComponent;
-    let testHostFixture: ComponentFixture<TestHostComponent>;
-    let testedComponent: CheckboxComponent;
-
-    beforeEach(waitForAsync(() => {
-        TestBed.configureTestingModule({
-            declarations: [CheckboxComponent, TestHostComponent],
-            imports: [FormsModule, ReactiveFormsModule]
-        });
-        testHostFixture = TestBed.createComponent(TestHostComponent);
-        testHostComponent = testHostFixture.componentInstance;
-        testHostFixture.detectChanges();
-        testedComponent = testHostComponent.testedComponent;
-    }));
-
-    it('should create the component', () => {
-        expect(testedComponent).toBeTruthy();
-    });
-
-    it('#getDefault() should not fill and enable form if criterion not defined', () => {
-        expect(testedComponent.checkboxes.length).toEqual(3);
-        expect(testedComponent.isChecked()).toBeFalsy();
-        expect(testedComponent.checkboxes.enabled).toBeTruthy();
-    });
-
-    it('#getDefault() should fill and disable form if criterion is defined', () => {
-        const options: Option[] = [
-            { label: 'One', value: 'one', display: 1 },
-            { label: 'Two', value: 'two', display: 2 }
-        ];
-        const criterion = { id: 1, type: 'multiple', options} as SelectMultipleCriterion;
-        testedComponent.getDefault(criterion);
-        expect(testedComponent.isChecked()).toBeTruthy();
-        const values = testedComponent.checkboxes.value as boolean[];
-        expect(values.filter(v => v).length).toEqual(2);
-    });
-
-    it('raises the add criterion event when clicked', () => {
-        testedComponent.id = 1;
-        testedComponent.checkboxes = new FormArray([
-            new FormControl(false),
-            new FormControl(true),
-            new FormControl(true)
-        ])
-        const expectedCriterion = {
-            id: 1,
-            type: 'multiple',
-            options: [
-                { label: 'Two', value: 'two', display: 2 },
-                { label: 'Three', value: 'three', display: 3 }
-            ]
-        };
-        testedComponent.addCriterion.subscribe((event: SelectMultipleCriterion) => expect(event).toEqual(expectedCriterion));
-        testedComponent.emitAdd();
-    });
-
-    it('raises the delete criterion event when clicked', () => {
-        testedComponent.id = 1;
-        testedComponent.deleteCriterion.subscribe((event: number) => expect(event).toEqual(1));
-        testedComponent.emitDelete();
-    });
-});
diff --git a/client/src/app/instance/search/components/criteria/search-type/datalist.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/datalist.component.spec.ts
deleted file mode 100644
index 588ac8c5304030b4386af241390533238f912512..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/datalist.component.spec.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
-import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
-
-import { DatalistComponent } from './datalist.component';
-import { FieldCriterion } from '../../../store/model';
-
-describe('[Search][Criteria][SearchType] Component: DatalistComponent', () => {
-    @Component({
-        selector: `app-host`,
-        template: `<app-datalist [criterion]='criterion'></app-datalist>`
-    })
-    class TestHostComponent {
-        @ViewChild(DatalistComponent, { static: false })
-        public testedComponent: DatalistComponent;
-        public criterion: FieldCriterion = undefined;
-    }
-
-    @Component({ selector: 'app-operator', template: '' })
-    class OperatorStubComponent {
-        @Input() operator: string;
-        @Input() searchType: string;
-        @Input() advancedForm: boolean;
-        @Input() disabled: boolean;
-        @Output() changeOperator: EventEmitter<string> = new EventEmitter();
-    }
-
-    @Component({ selector: 'app-help-like', template: '' })
-    class HelpLikeStubComponent { }
-
-    let testHostComponent: TestHostComponent;
-    let testHostFixture: ComponentFixture<TestHostComponent>;
-    let testedComponent: DatalistComponent;
-
-    beforeEach(waitForAsync(() => {
-        TestBed.configureTestingModule({
-            declarations: [
-                DatalistComponent,
-                TestHostComponent,
-                OperatorStubComponent,
-                HelpLikeStubComponent
-            ],
-            imports: [FormsModule, ReactiveFormsModule]
-        });
-        testHostFixture = TestBed.createComponent(TestHostComponent);
-        testHostComponent = testHostFixture.componentInstance;
-        testHostFixture.detectChanges();
-        testedComponent = testHostComponent.testedComponent;
-    }));
-
-    it('should create the component', () => {
-        expect(testedComponent).toBeTruthy();
-    });
-
-    it('#getDefault() should enable and not fill form as criterion not defined in host component', () => {
-        expect(testedComponent.field.value).toBeNull();
-        expect(testedComponent.field.enabled).toBeTruthy();
-    });
-
-    it('#getDefault() should fill and disable form if criterion is defined', () => {
-        const value = '10';
-        const criterion = { id: 1, type: 'field', operator: 'eq', value } as FieldCriterion;
-        const expectedFieldValue = value;
-        testedComponent.getDefault(criterion);
-        expect(testedComponent.field.value).toEqual(expectedFieldValue);
-        expect(testedComponent.field.disabled).toBeTruthy();
-    });
-
-    it('#getPlaceholder() should fill the placeholder if defined', () => {
-        const placeholder = '10';
-        testedComponent.placeholder = placeholder;
-        expect(testedComponent.getPlaceholder()).toEqual(placeholder);
-    });
-
-    it('#getPlaceholder() should not fill the placeholder if not defined', () => {
-        expect(testedComponent.getPlaceholder()).toEqual('');
-    });
-
-    it('#getDatalistId() should return an id', () => {
-        testedComponent.id = 1;
-        expect(testedComponent.getDatalistId()).toEqual('datalist_' + 1);
-    });
-
-    it('#changeOperator() should change the operator', () => {
-        expect(testedComponent.operator).toBeUndefined();
-        testedComponent.changeOperator('toto');
-        expect(testedComponent.operator).toBe('toto');
-    });
-
-    it('raises the add criterion event when clicked', () => {
-        testedComponent.id = 1;
-        const operator = '=';
-        const value = '10';
-        testedComponent.field = new FormControl(value);
-        testedComponent.operator = operator;
-        const expectedCriterion = { id: testedComponent.id, type: 'field', operator, value } as FieldCriterion;
-        testedComponent.addCriterion.subscribe((event: FieldCriterion) => expect(event).toEqual(expectedCriterion));
-        testedComponent.emitAdd();
-    });
-
-    it('raises the delete criterion event when clicked', () => {
-        testedComponent.id = 1;
-        testedComponent.deleteCriterion.subscribe((event: number) => expect(event).toEqual(1));
-        testedComponent.emitDelete();
-    });
-});
diff --git a/client/src/app/instance/search/components/criteria/search-type/date.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/date.component.spec.ts
deleted file mode 100644
index a67b65f674076ee75448da86877a1bdc093317f2..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/date.component.spec.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
-
-import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
-
-import { DateComponent } from './date.component';
-import { FieldCriterion } from '../../../store/model';
-
-describe('[Search][Criteria][SearchType] Component: DateComponent', () => {
-    @Component({
-        selector: `app-host`,
-        template: `<app-date [criterion]='criterion'></app-date>`
-    })
-    class TestHostComponent {
-        @ViewChild(DateComponent, { static: false })
-        public testedComponent: DateComponent;
-        public criterion: FieldCriterion = undefined;
-    }
-
-    @Component({ selector: 'app-operator', template: '' })
-    class OperatorStubComponent {
-        @Input() operator: string;
-        @Input() searchType: string;
-        @Input() advancedForm: boolean;
-        @Input() disabled: boolean;
-        @Output() changeOperator: EventEmitter<string> = new EventEmitter();
-    }
-
-    let testHostComponent: TestHostComponent;
-    let testHostFixture: ComponentFixture<TestHostComponent>;
-    let testedComponent: DateComponent;
-
-    beforeEach(waitForAsync(() => {
-        TestBed.configureTestingModule({
-            declarations: [
-                DateComponent, 
-                TestHostComponent,
-                OperatorStubComponent
-            ],
-            imports: [BsDatepickerModule.forRoot(), FormsModule, ReactiveFormsModule]
-        });
-        testHostFixture = TestBed.createComponent(TestHostComponent);
-        testHostComponent = testHostFixture.componentInstance;
-        testHostFixture.detectChanges();
-        testedComponent = testHostComponent.testedComponent;
-    }));
-
-    it('should create the component', () => {
-        expect(testedComponent).toBeTruthy();
-    });
-
-    it('#getDefault() should enable and not fill form as criterion not defined in host component', () => {
-        expect(testedComponent.field.value).toBeNull();
-        expect(testedComponent.field.enabled).toBeTruthy();
-    });
-
-    it('#getDefault() should fill and disable form if criterion is defined', () => {
-        const value = '2019-02-17';
-        const criterion = { id: 1, type: 'field', operator: 'eq', value } as FieldCriterion;
-        const expectedFieldValue = new Date(value);
-        testedComponent.getDefault(criterion);
-        expect(testedComponent.field.value).toEqual(expectedFieldValue);
-        expect(testedComponent.field.disabled).toBeTruthy();
-    });
-
-    it('#getPlaceholder() should fill the placeholder if defined', () => {
-        const placeholder = '2019-02-17';
-        testedComponent.placeholder = placeholder;
-        expect(testedComponent.getPlaceholder()).toEqual(placeholder);
-    });
-
-    it('#getPlaceholder() should not fill the placeholder if not defined', () => {
-        expect(testedComponent.getPlaceholder()).toEqual('');
-    });
-
-    it('#getDateString() should return a date as string', () => {
-        const dateString = '2019-02-17';
-        const date = new Date(dateString);
-        expect(testedComponent.getDateString(date)).toEqual(dateString);
-    });
-
-    it('#changeOperator() should change the operator', () => {
-        expect(testedComponent.operator).toBeUndefined();
-        testedComponent.changeOperator('toto');
-        expect(testedComponent.operator).toBe('toto');
-    });
-
-    it('raises the add criterion event when clicked', () => {
-        testedComponent.id = 1;
-        const operator = '=';
-        testedComponent.operator = operator;
-        const date = '2019-02-17';
-        testedComponent.field = new FormControl(new Date(date));
-        const expectedCriterion = { id: testedComponent.id, type: 'field', operator, value: date } as FieldCriterion;
-        testedComponent.addCriterion.subscribe((event: FieldCriterion) => expect(event).toEqual(expectedCriterion));
-        testedComponent.emitAdd();
-    });
-
-    it('raises the delete criterion event when clicked', () => {
-        testedComponent.id = 1;
-        testedComponent.deleteCriterion.subscribe((event: number) => expect(event).toEqual(1));
-        testedComponent.emitDelete();
-    });
-});
diff --git a/client/src/app/instance/search/components/criteria/search-type/datetime.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/datetime.component.spec.ts
deleted file mode 100644
index 045aeeeeee9bdf4ea743d8308504869ba35e73b3..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/datetime.component.spec.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
-
-import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
-import { NgSelectModule } from '@ng-select/ng-select';
-
-import { DatetimeComponent } from './datetime.component';
-import { FieldCriterion } from '../../../store/model';
-
-describe('[Search][Criteria][SearchType] Component: DatetimeComponent', () => {
-    @Component({
-        selector: `app-host`,
-        template: `<app-datetime [criterion]='criterion'></app-datetime>`
-    })
-    class TestHostComponent {
-        @ViewChild(DatetimeComponent, { static: false })
-        public testedComponent: DatetimeComponent;
-        public criterion: FieldCriterion = undefined;
-    }
-
-    @Component({ selector: 'app-operator', template: '' })
-    class OperatorStubComponent {
-        @Input() operator: string;
-        @Input() searchType: string;
-        @Input() advancedForm: boolean;
-        @Input() disabled: boolean;
-        @Output() changeOperator: EventEmitter<string> = new EventEmitter();
-    }
-
-    let testHostComponent: TestHostComponent;
-    let testHostFixture: ComponentFixture<TestHostComponent>;
-    let testedComponent: DatetimeComponent;
-
-    beforeEach(waitForAsync(() => {
-        TestBed.configureTestingModule({
-            declarations: [
-                DatetimeComponent,
-                TestHostComponent,
-                OperatorStubComponent
-            ],
-            imports: [BsDatepickerModule.forRoot(), NgSelectModule, FormsModule, ReactiveFormsModule]
-        });
-        testHostFixture = TestBed.createComponent(TestHostComponent);
-        testHostComponent = testHostFixture.componentInstance;
-        testHostFixture.detectChanges();
-        testedComponent = testHostComponent.testedComponent;
-    }));
-
-    it('should create the component', () => {
-        expect(testedComponent).toBeTruthy();
-    });
-
-    it('#getDefault() should enable and not fill form as criterion not defined in host component', () => {
-        expect(testedComponent.date.value).toBeNull();
-        expect(testedComponent.date.enabled).toBeTruthy();
-        expect(testedComponent.hh.value).toBeNull();
-        expect(testedComponent.hh.enabled).toBeTruthy();
-        expect(testedComponent.mm.value).toBeNull();
-        expect(testedComponent.mm.enabled).toBeTruthy();
-        expect(testedComponent.isValidFields).toBeFalsy();
-    });
-
-    it('#getDefault() should fill and disable form if criterion is defined', () => {
-        const value = '2019-02-17 15:47';
-        const criterion = { id: 1, type: 'field', operator: 'eq', value } as FieldCriterion;
-        const expectedDate = new Date('2019-02-17');
-        const expectedHour = '15';
-        const expectedMinute = '47';
-        testedComponent.getDefault(criterion);
-        expect(testedComponent.date.value).toEqual(expectedDate);
-        expect(testedComponent.date.disabled).toBeTruthy();
-        expect(testedComponent.hh.value).toEqual(expectedHour);
-        expect(testedComponent.hh.disabled).toBeTruthy();
-        expect(testedComponent.mm.value).toEqual(expectedMinute);
-        expect(testedComponent.mm.disabled).toBeTruthy();
-        expect(testedComponent.isValidFields).toBeTruthy();
-    });
-
-    it('#initTime(t) should return an array of string with 2 digits from 0 to t', () => {
-        const n = 10;
-        expect(testedComponent.initTime(n).length).toEqual(n);
-        expect(testedComponent.initTime(n)[5]).toEqual('05');
-    });
-
-    it('#change() should set #isValidFields to false if one or more fields are not defined', () => {
-        testedComponent.change();
-        expect(testedComponent.isValidFields).toBeFalsy();
-        testedComponent.hh = new FormControl('15');
-        testedComponent.change();
-        expect(testedComponent.isValidFields).toBeFalsy();
-        testedComponent.mm = new FormControl('47');
-        testedComponent.change();
-        expect(testedComponent.isValidFields).toBeFalsy();
-        testedComponent.date = new FormControl(new Date('2019-02-17'));
-        testedComponent.change();
-        expect(testedComponent.isValidFields).toBeTruthy();
-    });
-
-    it('#change() should set #datetime with fields value when defined', () => {
-        testedComponent.date = new FormControl(new Date('2019-02-17'));
-        testedComponent.hh = new FormControl('15');
-        testedComponent.mm = new FormControl('47');
-        testedComponent.change();
-        expect(testedComponent.datetime).toEqual(new Date('2019-02-17 15:47'));
-    });
-
-    it('#changeOperator() should change the operator', () => {
-        expect(testedComponent.operator).toBeUndefined();
-        testedComponent.changeOperator('toto');
-        expect(testedComponent.operator).toBe('toto');
-    });
-
-    it('raises the add criterion event when clicked', () => {
-        testedComponent.id = 1;
-        const operator = '=';
-        testedComponent.operator = operator;
-        testedComponent.datetime = new Date('2019-02-17 15:47');
-        testedComponent.hh = new FormControl('15');
-        testedComponent.mm = new FormControl('47');
-        const expectedCriterion = { id: testedComponent.id, type: 'field', operator, value: '2019-02-17 15:47' } as FieldCriterion;
-        testedComponent.addCriterion.subscribe((event: FieldCriterion) => expect(event).toEqual(expectedCriterion));
-        testedComponent.emitAdd();
-    });
-
-    it('raises the delete criterion event when clicked', () => {
-        testedComponent.id = 1;
-        testedComponent.deleteCriterion.subscribe((event: number) => expect(event).toEqual(1));
-        testedComponent.emitDelete();
-    });
-});
diff --git a/client/src/app/instance/search/components/criteria/search-type/field.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/field.component.spec.ts
deleted file mode 100644
index 4c7d55122bb30cd3b326b0de96acaf49da61cc6a..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/field.component.spec.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
-
-import { FieldComponent } from './field.component';
-import { FieldCriterion } from '../../../store/model';
-
-describe('[Search][Criteria][SearchType] Component: FieldComponent', () => {
-    @Component({
-        selector: `app-host`,
-        template: `<app-field [criterion]='criterion'></app-field>`
-    })
-    class TestHostComponent {
-        @ViewChild(FieldComponent, { static: false })
-        public testedComponent: FieldComponent;
-        public criterion: FieldCriterion = undefined;
-    }
-
-    @Component({ selector: 'app-operator', template: '' })
-    class OperatorStubComponent {
-        @Input() operator: string;
-        @Input() searchType: string;
-        @Input() advancedForm: boolean;
-        @Input() disabled: boolean;
-        @Output() changeOperator: EventEmitter<string> = new EventEmitter();
-    }
-
-    @Component({ selector: 'app-help-like', template: '' })
-    class HelpLikeStubComponent { }
-
-    let testHostComponent: TestHostComponent;
-    let testHostFixture: ComponentFixture<TestHostComponent>;
-    let testedComponent: FieldComponent;
-
-    beforeEach(waitForAsync(() => {
-        TestBed.configureTestingModule({
-            declarations: [
-                FieldComponent,
-                TestHostComponent,
-                OperatorStubComponent,
-                HelpLikeStubComponent
-            ],
-            imports: [FormsModule, ReactiveFormsModule]
-        });
-        testHostFixture = TestBed.createComponent(TestHostComponent);
-        testHostComponent = testHostFixture.componentInstance;
-        testHostFixture.detectChanges();
-        testedComponent = testHostComponent.testedComponent;
-    }));
-
-    it('should create the component', () => {
-        expect(testedComponent).toBeTruthy();
-    });
-
-    it('#getDefault() should enable and not fill form as criterion not defined in host component', () => {
-        expect(testedComponent.field.value).toBeNull();
-        expect(testedComponent.field.enabled).toBeTruthy();
-    });
-
-    it('#getDefault() should fill and disable form if criterion is defined', () => {
-        const value = 'test';
-        const criterion = { id: 1, type: 'field', operator: 'eq', value } as FieldCriterion;
-        const expectedFieldValue = value;
-        testedComponent.getDefault(criterion);
-        expect(testedComponent.field.value).toEqual(expectedFieldValue);
-        expect(testedComponent.field.disabled).toBeTruthy();
-    });
-
-    it('#getType() should return `number` if criterion is a number type', () => {
-        testedComponent.attributeType = 'smallint';
-        expect(testedComponent.getType()).toEqual('number');
-        testedComponent.attributeType = 'integer';
-        expect(testedComponent.getType()).toEqual('number');
-        testedComponent.attributeType = 'decimal';
-        expect(testedComponent.getType()).toEqual('number');
-        testedComponent.attributeType = 'float';
-        expect(testedComponent.getType()).toEqual('number');
-    });
-
-    it('#getType() should return `text` if criterion is not a number type', () => {
-        testedComponent.attributeType = 'char';
-        expect(testedComponent.getType()).toEqual('text');
-    });
-
-    it('#getPlaceholder() should fill the placeholder if defined', () => {
-        const placeholder = 'placeholder';
-        testedComponent.placeholder = placeholder;
-        expect(testedComponent.getPlaceholder()).toEqual(placeholder);
-    });
-
-    it('#getPlaceholder() should not fill the placeholder if not defined', () => {
-        expect(testedComponent.getPlaceholder()).toEqual('');
-    });
-
-    it('#changeOperator() should change the operator', () => {
-        expect(testedComponent.operator).toBeUndefined();
-        testedComponent.changeOperator('toto');
-        expect(testedComponent.operator).toBe('toto');
-    });
-
-    it('raises the add criterion event when clicked', () => {
-        testedComponent.id = 1;
-        const operator = '=';
-        testedComponent.operator = operator;
-        const value = 'test';
-        testedComponent.field = new FormControl(value);
-        const expectedCriterion = { id: testedComponent.id, type: 'field', operator, value } as FieldCriterion;
-        testedComponent.addCriterion.subscribe((event: FieldCriterion) => expect(event).toEqual(expectedCriterion));
-        testedComponent.emitAdd();
-    });
-
-    it('raises the delete criterion event when clicked', () => {
-        testedComponent.id = 1;
-        testedComponent.deleteCriterion.subscribe((event: number) => expect(event).toEqual(1));
-        testedComponent.emitDelete();
-    });
-});
diff --git a/client/src/app/instance/search/components/criteria/search-type/help-like.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/help-like.component.spec.ts
deleted file mode 100644
index 62a5a61175432e293174d02ce10b91c589b5a62f..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/help-like.component.spec.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { HelpLikeComponent } from './help-like.component';
-
-describe('[Search][Criteria][SearchType] Component: HelpLikeComponent', () => {
-    let component: HelpLikeComponent;
-    let fixture: ComponentFixture<HelpLikeComponent>;
-
-    beforeEach(() => {
-        TestBed.configureTestingModule({
-            declarations: [HelpLikeComponent]
-        });
-        fixture = TestBed.createComponent(HelpLikeComponent);
-        component = fixture.componentInstance;
-    });
-
-    it('should create the component', () => {
-        expect(component).toBeTruthy();
-    });
-});
diff --git a/client/src/app/instance/search/components/criteria/search-type/json.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/json.component.spec.ts
deleted file mode 100644
index 29767ff55c53ba7454cfa1e90a38f401fc4ba24b..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/json.component.spec.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { Component, ViewChild } from '@angular/core';
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { FormsModule, ReactiveFormsModule } from '@angular/forms';
-
-import { JsonComponent } from './json.component';
-import { JsonCriterion } from '../../../store/model';
-
-describe('[Search][Criteria][SearchType] Component: JsonComponent', () => {
-    @Component({
-        selector: `app-host`,
-        template: `<app-json-criteria [criterion]='criterion'></app-json-criteria>`
-    })
-    class TestHostComponent {
-        @ViewChild(JsonComponent, { static: false })
-        public testedComponent: JsonComponent;
-        public criterion: JsonCriterion = undefined;
-    }
-
-    let testHostComponent: TestHostComponent;
-    let testHostFixture: ComponentFixture<TestHostComponent>;
-    let testedComponent: JsonComponent;
-
-    beforeEach(waitForAsync(() => {
-        TestBed.configureTestingModule({
-            declarations: [JsonComponent, TestHostComponent],
-            imports: [FormsModule, ReactiveFormsModule]
-        });
-        testHostFixture = TestBed.createComponent(TestHostComponent);
-        testHostComponent = testHostFixture.componentInstance;
-        testHostFixture.detectChanges();
-        testedComponent = testHostComponent.testedComponent;
-    }));
-
-    it('should create the component', () => {
-        expect(testedComponent).toBeTruthy();
-    });
-
-    it('#getDefault() should enable and not fill form as criterion not defined in host component', () => {
-        expect(testedComponent.jsonForm.controls.path.value).toBeNull();
-        expect(testedComponent.jsonForm.controls.operator.value).toBeNull();
-        expect(testedComponent.jsonForm.controls.value.value).toBeNull();
-        expect(testedComponent.jsonForm.enabled).toBeTruthy();
-    });
-
-    it('#getDefault() should fill and disable form if criterion is defined', () => {
-        const path = 'path';
-        const operator = '=';
-        const value = 'test';
-        const criterion = { id: 1, type: 'json', path, operator, value } as JsonCriterion;
-        testedComponent.getDefault(criterion);
-        expect(testedComponent.jsonForm.controls.path.value).toEqual(path);
-        expect(testedComponent.jsonForm.controls.operator.value).toEqual(operator);
-        expect(testedComponent.jsonForm.controls.value.value).toEqual(value);
-        expect(testedComponent.jsonForm.disabled).toBeTruthy();
-    });
-
-    it('raises the add criterion event when clicked', () => {
-        testedComponent.id = 1;
-        const path = 'path';
-        const operator = '=';
-        const value = 'test';
-        testedComponent.jsonForm.controls.path.setValue(path);
-        testedComponent.jsonForm.controls.operator.setValue(operator);
-        testedComponent.jsonForm.controls.value.setValue(value);
-        const expectedCriterion = { id: testedComponent.id, type: 'json', path, operator, value } as JsonCriterion;
-        testedComponent.addCriterion.subscribe((event: JsonCriterion) => expect(event).toEqual(expectedCriterion));
-        testedComponent.emitAdd();
-    });
-
-    it('raises the delete criterion event when clicked', () => {
-        testedComponent.id = 1;
-        testedComponent.deleteCriterion.subscribe((event: number) => expect(event).toEqual(1));
-        testedComponent.emitDelete();
-    });
-});
diff --git a/client/src/app/instance/search/components/criteria/search-type/list.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/list.component.spec.ts
deleted file mode 100644
index d08f64b79485959e4474efe49f989faad0b23ca3..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/list.component.spec.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { Component, ViewChild } from '@angular/core';
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
-
-import { ListComponent } from './list.component';
-import { ListCriterion } from '../../../store/model';
-
-describe('[Search][Criteria][SearchType] Component: ListComponent', () => {
-    @Component({
-        selector: `app-host`,
-        template: `<app-list [criterion]='criterion'></app-list>`
-    })
-    class TestHostComponent {
-        @ViewChild(ListComponent, { static: false })
-        public testedComponent: ListComponent;
-        public criterion: ListComponent = undefined;
-    }
-
-    let testHostComponent: TestHostComponent;
-    let testHostFixture: ComponentFixture<TestHostComponent>;
-    let testedComponent: ListComponent;
-
-    beforeEach(waitForAsync(() => {
-        TestBed.configureTestingModule({
-            declarations: [
-                ListComponent,
-                TestHostComponent
-            ],
-            imports: [FormsModule, ReactiveFormsModule]
-        });
-        testHostFixture = TestBed.createComponent(TestHostComponent);
-        testHostComponent = testHostFixture.componentInstance;
-        testHostFixture.detectChanges();
-        testedComponent = testHostComponent.testedComponent;
-    }));
-
-    it('should create the component', () => {
-        expect(testedComponent).toBeTruthy();
-    });
-
-    it('#getDefault() should enable and not fill form as criterion not defined in host component', () => {
-        expect(testedComponent.list.value).toBeNull();
-        expect(testedComponent.list.enabled).toBeTruthy();
-    });
-
-    it('#getDefault() should fill and disable form if criterion is defined', () => {
-        const values = ['1', '2'];
-        const criterion = { id: 1, type: 'list', values } as ListCriterion;
-        const expectedListValues = values.join('\n');
-        testedComponent.getDefault(criterion);
-        expect(testedComponent.list.value).toBe(expectedListValues);
-        expect(testedComponent.list.disabled).toBeTruthy();
-    });
-
-    it('#getPlaceholder() should fill the placeholder if defined', () => {
-        const placeholder = 'placeholder';
-        testedComponent.placeholder = placeholder;
-        expect(testedComponent.getPlaceholder()).toBe(placeholder);
-    });
-
-    it('#getPlaceholder() should not fill the placeholder if not defined', () => {
-        expect(testedComponent.getPlaceholder()).toBe('');
-    });
-
-    it('raises the add criterion event when clicked', () => {
-        testedComponent.id = 1;
-        const values = '1\n2';
-        testedComponent.list = new FormControl(values);
-        const expectedCriterion = { id: testedComponent.id, type: 'list', values: ['1', '2'] } as ListCriterion;
-        testedComponent.addCriterion.subscribe((event: ListCriterion) => expect(event).toEqual(expectedCriterion));
-        testedComponent.emitAdd();
-    });
-});
diff --git a/client/src/app/instance/search/components/criteria/search-type/operator.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/operator.component.spec.ts
deleted file mode 100644
index cec4db0981a0806e7dd6112bea0e1c8e75cc3b7b..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/operator.component.spec.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { Component, ViewChild } from '@angular/core';
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { FormsModule, ReactiveFormsModule } from '@angular/forms';
-
-import { OperatorComponent } from './operator.component';
-
-describe('[Search][Criteria][SearchType] Component: OperatorComponent', () => {
-    @Component({
-        selector: `app-host`,
-        template: `
-            <app-operator
-                [operator]='operator'
-                [searchType]='searchType'
-                [advancedForm]='advancedForm'
-                [disabled]='disabled'>
-            </app-operator>`
-    })
-    class TestHostComponent {
-        @ViewChild(OperatorComponent, { static: false })
-        testedComponent: OperatorComponent;
-        operator = 'eq';
-        searchType: string = undefined;
-        advancedForm: boolean = undefined;
-        disabled: boolean = undefined;
-    }
-
-    let testHostComponent: TestHostComponent;
-    let testHostFixture: ComponentFixture<TestHostComponent>;
-    let testedComponent: OperatorComponent;
-
-    beforeEach(waitForAsync(() => {
-        TestBed.configureTestingModule({
-            declarations: [OperatorComponent, TestHostComponent],
-            imports: [FormsModule, ReactiveFormsModule]
-        });
-        testHostFixture = TestBed.createComponent(TestHostComponent);
-        testHostComponent = testHostFixture.componentInstance;
-        testHostFixture.detectChanges();
-        testedComponent = testHostComponent.testedComponent;
-    }));
-
-    it('should create the component', () => {
-        expect(testedComponent).toBeTruthy();
-    });
-
-    it('#getLabel() should return the correct operator form label', () => {
-        expect(testedComponent.getLabel('eq')).toBe('=');
-        expect(testedComponent.getLabel('neq')).toBe('≠');
-        expect(testedComponent.getLabel('gt')).toBe('>');
-        expect(testedComponent.getLabel('gte')).toBe('>=');
-        expect(testedComponent.getLabel('lt')).toBe('<');
-        expect(testedComponent.getLabel('lte')).toBe('<=');
-        expect(testedComponent.getLabel('lk')).toBe('like');
-        expect(testedComponent.getLabel('nlk')).toBe('not like');
-    });
-
-    it('raises the changeOperator event when the value change', () => {
-        testedComponent.changeOperator.subscribe((event: string) => expect(event).toEqual('eq'));
-        testedComponent.emitChange('eq');
-    });
-
-    it('should display the select box when it\'s an enabled advanced form', () => {
-        testHostComponent.advancedForm = true;
-        testHostComponent.disabled = false;
-        testHostFixture.detectChanges();
-        const template = testHostFixture.nativeElement;
-        expect(template.querySelector('select')).toBeTruthy();
-        expect(template.querySelector('.readonly')).toBeFalsy();
-    });
-
-    it('should display the readonly field when it\'s not an advanced form or not enabled advanced form', () => {
-        testHostComponent.advancedForm = true;
-        testHostComponent.disabled = true;
-        testHostFixture.detectChanges();
-        const template = testHostFixture.nativeElement;
-        expect(template.querySelector('select')).toBeFalsy();
-        expect(template.querySelector('.readonly')).toBeTruthy();
-        testHostComponent.advancedForm = true;
-        testHostFixture.detectChanges();
-        expect(template.querySelector('select')).toBeFalsy();
-        expect(template.querySelector('.readonly')).toBeTruthy();
-    });
-});
diff --git a/client/src/app/instance/search/components/criteria/search-type/radio.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/radio.component.spec.ts
deleted file mode 100644
index e6b5658f4821e904e6942dd10bbc921450524d73..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/radio.component.spec.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { Component, ViewChild } from '@angular/core';
-import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
-
-import { RadioComponent } from './radio.component';
-import { FieldCriterion } from '../../../store/model';
-
-describe('[Search][Criteria][SearchType] Component: RadioComponent', () => {
-    @Component({
-        selector: `app-host`,
-        template: `<app-radio [criterion]='criterion'></app-radio>`
-    })
-    class TestHostComponent {
-        @ViewChild(RadioComponent, { static: false })
-        public testedComponent: RadioComponent;
-        public criterion: FieldCriterion = undefined;
-    }
-
-    let testHostComponent: TestHostComponent;
-    let testHostFixture: ComponentFixture<TestHostComponent>;
-    let testedComponent: RadioComponent;
-
-    beforeEach(waitForAsync(() => {
-        TestBed.configureTestingModule({
-            declarations: [RadioComponent, TestHostComponent],
-            imports: [FormsModule, ReactiveFormsModule]
-        });
-        testHostFixture = TestBed.createComponent(TestHostComponent);
-        testHostComponent = testHostFixture.componentInstance;
-        testHostFixture.detectChanges();
-        testedComponent = testHostComponent.testedComponent;
-    }));
-
-    it('should create the component', () => {
-        expect(testedComponent).toBeTruthy();
-    });
-
-    it('#getDefault() should enable and not fill form as criterion not defined in host component', () => {
-        expect(testedComponent.radio.value).toBeNull();
-        expect(testedComponent.radio.enabled).toBeTruthy();
-    });
-
-    it('#getDefault() should fill and disable form if criterion is defined', () => {
-        const operator = '=';
-        const value = 'test';
-        const criterion = { id: 1, type: 'field', operator, value } as FieldCriterion;
-        testedComponent.getDefault(criterion);
-        expect(testedComponent.radio.value).toEqual(value);
-        expect(testedComponent.radio.disabled).toBeTruthy();
-    });
-
-    it('raises the add criterion event when clicked', () => {
-        testedComponent.id = 1;
-        const operator = '=';
-        testedComponent.operator = operator;
-        const value = 'three';
-        testedComponent.radio = new FormControl(value);
-        testedComponent.options = [
-            { label: 'One', value: 'one', display: 1 },
-            { label: 'Two', value: 'two', display: 2 },
-            { label: 'Three', value: 'three', display: 3 }
-        ];
-        const expectedCriterion = { id: testedComponent.id, type: 'field', operator, value } as FieldCriterion;
-        testedComponent.addCriterion.subscribe((event: FieldCriterion) => expect(event).toEqual(expectedCriterion));
-        testedComponent.emitAdd();
-    });
-
-    it('raises the delete criterion event when clicked', () => {
-        testedComponent.id = 1;
-        testedComponent.deleteCriterion.subscribe((event: number) => expect(event).toEqual(1));
-        testedComponent.emitDelete();
-    });
-});
diff --git a/client/src/app/instance/search/components/criteria/search-type/select-multiple.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/select-multiple.component.spec.ts
deleted file mode 100644
index e6f8ccfd504802a0a1b8031e59e4dc4a86326dc1..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/select-multiple.component.spec.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { Component, ViewChild } from '@angular/core';
-import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
-
-import { NgSelectModule } from '@ng-select/ng-select';
-
-import { SelectMultipleComponent } from './select-multiple.component';
-import { SelectMultipleCriterion } from '../../../store/model';
-
-describe('[Search][Criteria][SearchType] Component: SelectMultipleComponent', () => {
-    @Component({
-        selector: `app-host`,
-        template: `<app-select-multiple [criterion]='criterion'></app-select-multiple>`
-    })
-    class TestHostComponent {
-        @ViewChild(SelectMultipleComponent, { static: false })
-        public testedComponent: SelectMultipleComponent;
-        public criterion: SelectMultipleCriterion = undefined;
-    }
-
-    let testHostComponent: TestHostComponent;
-    let testHostFixture: ComponentFixture<TestHostComponent>;
-    let testedComponent: SelectMultipleComponent;
-
-    beforeEach(waitForAsync(() => {
-        TestBed.configureTestingModule({
-            declarations: [SelectMultipleComponent, TestHostComponent],
-            imports: [NgSelectModule, FormsModule, ReactiveFormsModule]
-        });
-        testHostFixture = TestBed.createComponent(TestHostComponent);
-        testHostComponent = testHostFixture.componentInstance;
-        testHostFixture.detectChanges();
-        testedComponent = testHostComponent.testedComponent;
-    }));
-
-    it('should create the component', () => {
-        expect(testedComponent).toBeTruthy();
-    });
-
-    it('#getDefault() should enable and not fill form as criterion not defined in host component', () => {
-        expect(testedComponent.ms.value).toBeNull();
-        expect(testedComponent.ms.enabled).toBeTruthy();
-    });
-
-    it('#getDefault() should fill and disable form if criterion is defined', () => {
-        const options = [
-            { label: 'One', value: 'one', display: 1 },
-            { label: 'Two', value: 'two', display: 2 },
-            { label: 'Three', value: 'three', display: 3 }
-        ];
-        const criterion = { id: 1, type: 'multiple', options} as SelectMultipleCriterion;
-        testedComponent.getDefault(criterion);
-        expect(testedComponent.ms.value).toEqual(['one', 'two', 'three']);
-        expect(testedComponent.ms.disabled).toBeTruthy();
-    });
-
-    it('raises the add criterion event when clicked', () => {
-        testedComponent.id = 1;
-        testedComponent.options = [
-            { label: 'One', value: 'one', display: 1 },
-            { label: 'Two', value: 'two', display: 2 },
-            { label: 'Three', value: 'three', display: 3 }
-        ];
-        const value = ['three'];
-        testedComponent.ms = new FormControl(value);
-        const expectedValue = [
-            { label: 'Three', value: 'three', display: 3 }
-        ];
-        const expectedCriterion = { id: testedComponent.id, type: 'multiple', options: expectedValue } as SelectMultipleCriterion;
-        testedComponent.addCriterion.subscribe((event: SelectMultipleCriterion) => expect(event).toEqual(expectedCriterion));
-        testedComponent.emitAdd();
-    });
-
-    it('raises the delete criterion event when clicked', () => {
-        testedComponent.id = 1;
-        testedComponent.deleteCriterion.subscribe((event: number) => expect(event).toEqual(1));
-        testedComponent.emitDelete();
-    });
-});
diff --git a/client/src/app/instance/search/components/criteria/search-type/select.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/select.component.spec.ts
deleted file mode 100644
index 05f8edff0f509d817f66706bf62fe2fa7ae28bc7..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/select.component.spec.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
-
-import { NgSelectModule } from '@ng-select/ng-select';
-
-import { SelectComponent } from './select.component';
-import { FieldCriterion } from '../../../store/model';
-
-describe('[Search][Criteria][SearchType] Component: SelectComponent', () => {
-    @Component({
-        selector: `app-host`,
-        template: `<app-select [criterion]='criterion'></app-select>`
-    })
-    class TestHostComponent {
-        @ViewChild(SelectComponent, { static: false })
-        public testedComponent: SelectComponent;
-        public criterion: FieldCriterion = undefined;
-    }
-
-    @Component({ selector: 'app-operator', template: '' })
-    class OperatorStubComponent {
-        @Input() operator: string;
-        @Input() searchType: string;
-        @Input() advancedForm: boolean;
-        @Input() disabled: boolean;
-        @Output() changeOperator: EventEmitter<string> = new EventEmitter();
-    }
-
-    @Component({ selector: 'app-help-like', template: '' })
-    class HelpLikeStubComponent { }
-
-    let testHostComponent: TestHostComponent;
-    let testHostFixture: ComponentFixture<TestHostComponent>;
-    let testedComponent: SelectComponent;
-
-    beforeEach(waitForAsync(() => {
-        TestBed.configureTestingModule({
-            declarations: [
-                SelectComponent, 
-                TestHostComponent,
-                OperatorStubComponent,
-                HelpLikeStubComponent
-            ],
-            imports: [NgSelectModule, FormsModule, ReactiveFormsModule]
-        });
-        testHostFixture = TestBed.createComponent(TestHostComponent);
-        testHostComponent = testHostFixture.componentInstance;
-        testHostFixture.detectChanges();
-        testedComponent = testHostComponent.testedComponent;
-    }));
-
-    it('should create the component', () => {
-        expect(testedComponent).toBeTruthy();
-    });
-
-    it('#getDefault() should enable and not fill form as criterion not defined in host component', () => {
-        expect(testedComponent.se.value).toBeNull();
-        expect(testedComponent.se.enabled).toBeTruthy();
-    });
-
-    it('#getDefault() should fill and disable form if criterion is defined', () => {
-        const operator = '=';
-        const value = 'test';
-        const criterion = { id: 1, type: 'field', operator, value } as FieldCriterion;
-        testedComponent.getDefault(criterion);
-        expect(testedComponent.se.value).toEqual(value);
-        expect(testedComponent.se.disabled).toBeTruthy();
-    });
-
-    it('#changeOperator() should change the operator', () => {
-        expect(testedComponent.operator).toBeUndefined();
-        testedComponent.changeOperator('toto');
-        expect(testedComponent.operator).toBe('toto');
-    });
-
-    it('raises the add criterion event when clicked', () => {
-        testedComponent.id = 1;
-        const operator = '=';
-        testedComponent.operator = operator;
-        const value = 'three';
-        testedComponent.se = new FormControl(value);
-        testedComponent.options = [
-            { label: 'One', value: 'one', display: 1 },
-            { label: 'Two', value: 'two', display: 2 },
-            { label: 'Three', value: 'three', display: 3 }
-        ];
-        const expectedCriterion = { id: testedComponent.id, type: 'field', operator, value } as FieldCriterion;
-        testedComponent.addCriterion.subscribe((event: FieldCriterion) => expect(event).toEqual(expectedCriterion));
-        testedComponent.emitAdd();
-    });
-
-    it('raises the delete criterion event when clicked', () => {
-        testedComponent.id = 1;
-        testedComponent.deleteCriterion.subscribe((event: number) => expect(event).toEqual(1));
-        testedComponent.emitDelete();
-    });
-});
diff --git a/client/src/app/instance/search/components/criteria/search-type/time.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/time.component.spec.ts
deleted file mode 100644
index f93f526c832f091e3df4eed6eb2300a585751186..0000000000000000000000000000000000000000
--- a/client/src/app/instance/search/components/criteria/search-type/time.component.spec.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
-import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
-
-import { NgSelectModule } from '@ng-select/ng-select';
-
-import { TimeComponent } from './time.component';
-import { FieldCriterion } from '../../../store/model';
-
-describe('[Search][Criteria][SearchType] Component: TimeComponent', () => {
-    @Component({
-        selector: `app-host`,
-        template: `<app-time [criterion]='criterion'></app-time>`
-    })
-    class TestHostComponent {
-        @ViewChild(TimeComponent, { static: false })
-        public testedComponent: TimeComponent;
-        public criterion: FieldCriterion = undefined;
-    }
-
-    @Component({ selector: 'app-operator', template: '' })
-    class OperatorStubComponent {
-        @Input() operator: string;
-        @Input() searchType: string;
-        @Input() advancedForm: boolean;
-        @Input() disabled: boolean;
-        @Output() changeOperator: EventEmitter<string> = new EventEmitter();
-    }
-
-    let testHostComponent: TestHostComponent;
-    let testHostFixture: ComponentFixture<TestHostComponent>;
-    let testedComponent: TimeComponent;
-
-    beforeEach(waitForAsync(() => {
-        TestBed.configureTestingModule({
-            declarations: [
-                TimeComponent,
-                TestHostComponent,
-                OperatorStubComponent
-            ],
-            imports: [NgSelectModule, FormsModule, ReactiveFormsModule]
-        });
-        testHostFixture = TestBed.createComponent(TestHostComponent);
-        testHostComponent = testHostFixture.componentInstance;
-        testHostFixture.detectChanges();
-        testedComponent = testHostComponent.testedComponent;
-    }));
-
-    it('should create the component', () => {
-        expect(testedComponent).toBeTruthy();
-    });
-
-    it('#getDefault() should enable and not fill form as criterion not defined in host component', () => {
-        expect(testedComponent.hh.value).toBeNull();
-        expect(testedComponent.hh.enabled).toBeTruthy();
-        expect(testedComponent.mm.value).toBeNull();
-        expect(testedComponent.mm.enabled).toBeTruthy();
-    });
-
-    it('#getDefault() should fill and disable form if criterion is defined', () => {
-        const criterion = { id: 1, type: 'field', operator: 'eq', value: '15:47' } as FieldCriterion;
-        const expectedHour = '15';
-        const expectedMinute = '47';
-        testedComponent.getDefault(criterion);
-        expect(testedComponent.hh.value).toEqual(expectedHour);
-        expect(testedComponent.hh.disabled).toBeTruthy();
-        expect(testedComponent.mm.value).toEqual(expectedMinute);
-        expect(testedComponent.mm.disabled).toBeTruthy();
-    });
-
-    it('#initTime(t) should return an array of string with 2 digits from 0 to t', () => {
-        const n = 10;
-        expect(testedComponent.initTime(n).length).toEqual(n);
-        expect(testedComponent.initTime(n)[5]).toEqual('05');
-    });
-
-    it('#changeOperator() should change the operator', () => {
-        expect(testedComponent.operator).toBeUndefined();
-        testedComponent.changeOperator('toto');
-        expect(testedComponent.operator).toBe('toto');
-    });
-
-    it('raises the add criterion event when clicked', () => {
-        testedComponent.id = 1;
-        const operator = 'eq';
-        testedComponent.operator = operator;
-        testedComponent.hh = new FormControl('15');
-        testedComponent.mm = new FormControl('47');
-        const expectedCriterion = { id: testedComponent.id, type: 'field', operator, value: '15:47' } as FieldCriterion;
-        testedComponent.addCriterion.subscribe((event: FieldCriterion) => expect(event).toEqual(expectedCriterion));
-        testedComponent.emitAdd();
-    });
-
-    it('raises the delete criterion event when clicked', () => {
-        testedComponent.id = 1;
-        testedComponent.deleteCriterion.subscribe((event: number) => expect(event).toEqual(1));
-        testedComponent.emitDelete();
-    });
-});
diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.html b/client/src/app/instance/search/components/result/datatable-tab.component.html
index 670acae1dd47d1c6dc836cbd4f78e0cec4592d37..26dd36d1be6a174d083b150ebb580194511e8e9c 100644
--- a/client/src/app/instance/search/components/result/datatable-tab.component.html
+++ b/client/src/app/instance/search/components/result/datatable-tab.component.html
@@ -1,5 +1,5 @@
-<accordion *ngIf="getDataset().config.datatable.datatable_enabled" [isAnimated]="true">
-    <accordion-group #ag [isOpen]="getDataset().config.datatable.datatable_opened" [panelClass]="'custom-accordion'" class="my-2">
+<accordion *ngIf="(datasetList | datasetByName:datasetSelected).config.datatable.datatable_enabled" [isAnimated]="true">
+    <accordion-group #ag [isOpen]="(datasetList | datasetByName:datasetSelected).config.datatable.datatable_opened" [panelClass]="'custom-accordion'" class="my-2">
         <button class="btn btn-link btn-block clearfix" accordion-heading>
             <span class="pull-left float-left">
                 Display result details
@@ -9,7 +9,8 @@
             </span>
         </button>
         <app-datatable
-                [dataset]="getDataset()"
+                [dataset]="datasetList | datasetByName:datasetSelected"
+                [instanceSelected]="instanceSelected"
                 [attributeList]="attributeList"
                 [outputList]="outputList"
                 [dataLength]="dataLength"
diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.ts b/client/src/app/instance/search/components/result/datatable-tab.component.ts
index 0b7b2258095c4ff700fc940781e0ed6a4dd5d6af..c24e1979d36d8710d5efe016bd9ba69a5e4c35d2 100644
--- a/client/src/app/instance/search/components/result/datatable-tab.component.ts
+++ b/client/src/app/instance/search/components/result/datatable-tab.component.ts
@@ -23,6 +23,7 @@ import { Pagination } from 'src/app/instance/store/models';
  */
 export class DatatableTabComponent {
     @Input() datasetSelected: string;
+    @Input() instanceSelected: string;
     @Input() datasetList: Dataset[];
     @Input() attributeList: Attribute[];
     @Input() outputList: number[];
@@ -34,13 +35,4 @@ export class DatatableTabComponent {
     @Output() retrieveData: EventEmitter<Pagination> = new EventEmitter();
     @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter();
     @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter();
-
-    /**
-     * Returns selected dataset for the search.
-     *
-     * @return Dataset
-     */
-    getDataset(): Dataset {
-        return this.datasetList.find(dataset => dataset.name === this.datasetSelected);
-    }
 }
diff --git a/client/src/app/instance/search/components/result/download.component.ts b/client/src/app/instance/search/components/result/download.component.ts
index aedba8081c7c8c2412e7a397ddf8a12c0a0cc8c0..729dfe595cc1f1e8d5484bf2d91b84311d35ca44 100644
--- a/client/src/app/instance/search/components/result/download.component.ts
+++ b/client/src/app/instance/search/components/result/download.component.ts
@@ -13,6 +13,7 @@ import { Criterion, criterionToString } from '../../../store/models';
 import { Dataset } from 'src/app/metamodel/models';
 import { getHost as host } from 'src/app/shared/utils';
 // import { ConeSearch } from '../../../shared/cone-search/store/model';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Component({
     selector: 'app-download',
@@ -36,6 +37,8 @@ export class DownloadComponent {
     @Input() sampRegistered: boolean;
     @Output() broadcast: EventEmitter<string> = new EventEmitter();
 
+    constructor(private appConfig: AppConfigService) { }
+
     /**
      * Returns dataset label.
      *
@@ -76,7 +79,7 @@ export class DownloadComponent {
      * @return string
      */
     getUrl(format: string): string {
-        let query: string = host() + '/search/' + this.datasetSelected + '?a=' + this.outputList.join(';');
+        let query: string = host(this.appConfig.apiUrl) + '/search/' + this.datasetSelected + '?a=' + this.outputList.join(';');
         if (this.criteriaList.length > 0) {
             query += '&c=' + this.criteriaList.map(criterion => criterionToString(criterion)).join(';');
         }
@@ -88,7 +91,7 @@ export class DownloadComponent {
     }
 
     getUrlArchive(): string {
-        let query: string = host() + '/archive/' + this.datasetSelected + '?a=' + this.outputList.join(';');
+        let query: string = host(this.appConfig.apiUrl) + '/archive/' + this.datasetSelected + '?a=' + this.outputList.join(';');
         if (this.criteriaList.length > 0) {
             query += '&c=' + this.criteriaList.map(criterion => criterionToString(criterion)).join(';');
         }
diff --git a/client/src/app/instance/search/components/result/reminder.component.html b/client/src/app/instance/search/components/result/reminder.component.html
index d76e57d79282e0ffe5efefd8ab799a41ee6036e9..38372acef640c24c36328c786925527dd5ecfd8f 100644
--- a/client/src/app/instance/search/components/result/reminder.component.html
+++ b/client/src/app/instance/search/components/result/reminder.component.html
@@ -24,14 +24,14 @@
                         </div>
 
                         <div *ngIf="nbCriteria() > 0" class="row">
-                            <!-- <div *ngIf="isConeSearchAdded" class="col-12 col-md-6 col-xl-4 pb-3">
+                            <div *ngIf="coneSearch" class="col-12 col-md-6 col-xl-4 pb-3">
                                 <span class="title">Cone search</span>
                                 <ul class="list-unstyled pl-3">
                                     <li>RA = {{ coneSearch.ra }}°</li>
                                     <li>DEC = {{ coneSearch.dec }}°</li>
                                     <li>radius = {{ coneSearch.radius }} arcsecond</li>
                                 </ul>
-                            </div> -->
+                            </div>
 
                             <ng-container *ngFor="let family of criteriaFamilyList">
                                 <ng-container *ngIf="criteriaByFamily(family.id).length > 0">
diff --git a/client/src/app/instance/search/components/result/reminder.component.ts b/client/src/app/instance/search/components/result/reminder.component.ts
index 9961ece07a9ed7101ff1de5aa915187a94884628..8e3d88ea4ffc5584ba30b77ba2168c0a1e993cd0 100644
--- a/client/src/app/instance/search/components/result/reminder.component.ts
+++ b/client/src/app/instance/search/components/result/reminder.component.ts
@@ -28,9 +28,8 @@ export class ReminderComponent {
     @Input() criteriaFamilyList: CriteriaFamily[];
     @Input() outputFamilyList: OutputFamily[];
     @Input() outputCategoryList: OutputCategory[];
-    // @Input() isConeSearchAdded: boolean;
-    // @Input() coneSearch: ConeSearch;
     @Input() criteriaList: Criterion[];
+    @Input() coneSearch: ConeSearch;
     @Input() outputList: number[];
 
     isSummaryActivated(): boolean {
@@ -49,9 +48,9 @@ export class ReminderComponent {
      * @return number
      */
     nbCriteria(): number {
-        // if (this.isConeSearchAdded) {
-        //     return this.criteriaList.length + 1;
-        // }
+        if (this.coneSearch) {
+            return this.criteriaList.length + 1;
+        }
         return this.criteriaList.length;
     }
 
diff --git a/client/src/app/instance/search/components/result/url-display.component.ts b/client/src/app/instance/search/components/result/url-display.component.ts
index 469bc9b420a9710c4a959a99c088dbf01d572822..1ed461ba64d2d44b402b2cf8eaedf98d59e5024d 100644
--- a/client/src/app/instance/search/components/result/url-display.component.ts
+++ b/client/src/app/instance/search/components/result/url-display.component.ts
@@ -14,6 +14,7 @@ import { ToastrService } from 'ngx-toastr';
 import { Criterion, ConeSearch, criterionToString } from 'src/app/instance/store/models';
 import { Dataset } from 'src/app/metamodel/models';
 import { getHost as host } from 'src/app/shared/utils';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Component({
     selector: 'app-url-display',
@@ -32,7 +33,7 @@ export class UrlDisplayComponent {
     @Input() criteriaList: Criterion[];
     @Input() outputList: number[];
 
-    constructor(private toastr: ToastrService) { }
+    constructor(private toastr: ToastrService, private appConfig: AppConfigService) { }
 
     /**
      * Checks if URL display is enabled.
@@ -55,7 +56,7 @@ export class UrlDisplayComponent {
      * @return string
      */
     getUrl(): string {
-        let query: string = host() + '/search/' + this.datasetSelected + '?a=' + this.outputList.join(';');
+        let query: string = host(this.appConfig.apiUrl) + '/search/' + this.datasetSelected + '?a=' + this.outputList.join(';');
         if (this.criteriaList.length > 0) {
             query += '&c=' + this.criteriaList.map(criterion => criterionToString(criterion)).join(';');
         }
diff --git a/client/src/app/instance/search/components/summary.component.html b/client/src/app/instance/search/components/summary.component.html
index 0fc5455ed4c10d7ee751b4823bb686211b37d056..86c376fb03367425c7f6a4776396d78ee31c6422 100644
--- a/client/src/app/instance/search/components/summary.component.html
+++ b/client/src/app/instance/search/components/summary.component.html
@@ -15,14 +15,14 @@
     <p *ngIf="noCriteria()" class="pl-5 font-weight-bold">
         No selected criteria
     </p>
-    <!-- <span *ngIf="isConeSearchAdded" class="pl-5">
+    <span *ngIf="coneSearch" class="pl-5">
         Cone search:
         <ul class="ml-3 pl-5 list-unstyled">
             <li>RA = {{ coneSearch.ra }}°</li>
             <li>DEC = {{ coneSearch.dec }}°</li>
             <li>radius = {{ coneSearch.radius }} arcsecond</li>
         </ul>
-    </span> -->
+    </span>
     <ul *ngIf="criteriaList.length > 0" class="pl-5 list-unstyled">
         <li *ngFor="let criterion of criteriaList">
             {{ getAttribute(criterion.id).form_label }} {{ printCriterion(criterion) }}
diff --git a/client/src/app/instance/search/components/summary.component.ts b/client/src/app/instance/search/components/summary.component.ts
index dfa03f44f4c86f23bb6fb8ab10ad3e5a8600b462..3e76c6275872536dca5f00257e2969ea611ad3b3 100644
--- a/client/src/app/instance/search/components/summary.component.ts
+++ b/client/src/app/instance/search/components/summary.component.ts
@@ -9,9 +9,8 @@
 
 import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';
 
-import { Criterion, SearchQueryParams, getPrettyCriterion } from '../../store/models';
+import { Criterion, ConeSearch, SearchQueryParams, getPrettyCriterion } from '../../store/models';
 import { Attribute, Dataset, CriteriaFamily, OutputFamily, OutputCategory } from 'src/app/metamodel/models';
-// import { ConeSearch } from '../../shared/cone-search/store/model';
 
 @Component({
     selector: 'app-summary',
@@ -27,8 +26,6 @@ export class SummaryComponent {
     @Input() currentStep: string;
     @Input() datasetSelected: string;
     @Input() datasetList: Dataset[];
-    // @Input() isConeSearchAdded: boolean;
-    // @Input() coneSearch: ConeSearch;
     @Input() attributeList: Attribute[];
     @Input() criteriaFamilyList: CriteriaFamily[];
     @Input() outputFamilyList: OutputFamily[];
@@ -36,6 +33,7 @@ export class SummaryComponent {
     @Input() criteriaList: Criterion[];
     @Input() outputList: number[];
     @Input() queryParams: SearchQueryParams;
+    @Input() coneSearch: ConeSearch;
 
     accordionFamilyIsOpen = true;
 
@@ -54,8 +52,7 @@ export class SummaryComponent {
      * @return boolean
      */
     noCriteria(): boolean {
-        //if (this.isConeSearchAdded || this.criteriaList.length > 0) {
-        if (this.criteriaList.length > 0) {
+        if (this.coneSearch || this.criteriaList.length > 0) {
             return false
         }
         return true;
diff --git a/client/src/app/instance/search/containers/abstract-search.component.ts b/client/src/app/instance/search/containers/abstract-search.component.ts
index a6ebdbbebc3bb3175648b19ddadfb4adc037c880..6774ae781e2266fbb6a6eb2503c477954393eef0 100644
--- a/client/src/app/instance/search/containers/abstract-search.component.ts
+++ b/client/src/app/instance/search/containers/abstract-search.component.ts
@@ -3,7 +3,7 @@ import { Directive, OnInit, OnDestroy } from '@angular/core';
 import { Store } from '@ngrx/store';
 import { Observable, Subscription } from 'rxjs';
 
-import { Criterion, SearchQueryParams } from '../../store/models';
+import { ConeSearch, Criterion, SearchQueryParams } from '../../store/models';
 import { Dataset, Attribute, CriteriaFamily, OutputFamily, OutputCategory } from 'src/app/metamodel/models';
 
 import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector';
@@ -14,6 +14,7 @@ import * as outputFamilySelector from 'src/app/metamodel/selectors/output-family
 import * as outputCategorySelector from 'src/app/metamodel/selectors/output-category.selector';
 import * as searchActions from '../../store/actions/search.actions';
 import * as searchSelector from '../../store/selectors/search.selector';
+import * as coneSearchSelector from '../../store/selectors/cone-search.selector';
 
 @Directive()
 export abstract class AbstractSearchComponent implements OnInit, OnDestroy {
@@ -39,6 +40,7 @@ export abstract class AbstractSearchComponent implements OnInit, OnDestroy {
     public criteriaList: Observable<Criterion[]>;
     public outputList: Observable<number[]>;
     public queryParams: Observable<SearchQueryParams>;
+    public coneSearch: Observable<ConeSearch>;
 
     private attributeListIsLoadedSubscription: Subscription;
 
@@ -65,6 +67,7 @@ export abstract class AbstractSearchComponent implements OnInit, OnDestroy {
         this.criteriaList = this.store.select(searchSelector.selectCriteriaList);
         this.outputList = this.store.select(searchSelector.selectOutputList);
         this.queryParams = this.store.select(searchSelector.selectQueryParams);
+        this.coneSearch = this.store.select(coneSearchSelector.selectConeSearch);
     }
 
     ngOnInit() {
diff --git a/client/src/app/instance/search/containers/criteria.component.html b/client/src/app/instance/search/containers/criteria.component.html
index 5fb87883773d70c4baf88ec337f5320fec6bd919..8ff2d1d903ea462d493393f6efa68700811f4265 100644
--- a/client/src/app/instance/search/containers/criteria.component.html
+++ b/client/src/app/instance/search/containers/criteria.component.html
@@ -7,13 +7,17 @@
     && (criteriaFamilyListIsLoaded | async)
     && (attributeListIsLoaded | async)" class="row mt-4">
     <div class="col-12 col-md-8 col-lg-9">
-        <!-- <app-cone-search-tab
+        <app-cone-search-tab
             [datasetSelected]="datasetSelected | async"
             [datasetList]="datasetList | async"
-            [isConeSearchAdded]="isConeSearchAdded | async"
-            [isValidConeSearch]="isValidConeSearch | async"
-            (coneSearchAdded)="coneSearchAdded($event)">
-        </app-cone-search-tab> -->
+            [coneSearch]="coneSearch | async"
+            [resolver]="resolver | async"
+            [resolverIsLoading]="resolverIsLoading | async"
+            [resolverIsLoaded]="resolverIsLoaded | async"
+            (addConeSearch)="addConeSearch($event)"
+            (deleteConeSearch)="deleteConeSearch()"
+            (retrieveCoordinates)="retrieveCoordinates($event)">
+        </app-cone-search-tab>
         <app-criteria-tabs 
             [attributeList]="attributeList | async"
             [criteriaFamilyList]="criteriaFamilyList | async"
@@ -34,7 +38,8 @@
             [outputCategoryList]="outputCategoryList | async"
             [criteriaList]="criteriaList | async"
             [outputList]="outputList | async"
-            [queryParams]="queryParams | async">
+            [queryParams]="queryParams | async"
+            [coneSearch]="coneSearch | async">
         </app-summary>
     </div>
 </div>
@@ -47,7 +52,7 @@
         </a>
     </div>
     <div class="col col-auto">
-        <a routerLink="/search/output/{{ datasetSelected | async }}" [queryParams]="queryParams | async"
+        <a routerLink="/instance/{{ instanceSelected | async }}/search/output/{{ datasetSelected | async }}" [queryParams]="queryParams | async"
             class="btn btn-outline-primary">
             Output <span class="fas fa-arrow-right"></span>
         </a>
diff --git a/client/src/app/instance/search/containers/criteria.component.ts b/client/src/app/instance/search/containers/criteria.component.ts
index a226c24ed0131bc8d5bfba3dc12a8f590121fd29..b03d239dc5f4ac0bc2e9a84b79b23d81a2ce6cd8 100644
--- a/client/src/app/instance/search/containers/criteria.component.ts
+++ b/client/src/app/instance/search/containers/criteria.component.ts
@@ -9,9 +9,14 @@
 
 import { Component } from '@angular/core';
 
+import { Store } from '@ngrx/store';
+import { Observable } from 'rxjs';
+
 import { AbstractSearchComponent } from './abstract-search.component';
-import { Criterion } from '../../store/models';
+import { ConeSearch, Criterion, Resolver } from '../../store/models';
 import * as searchActions from '../../store/actions/search.actions';
+import * as coneSearchActions from '../../store/actions/cone-search.actions';
+import * as coneSearchSelector from '../../store/selectors/cone-search.selector';
 
 @Component({
     selector: 'app-criteria',
@@ -22,6 +27,17 @@ import * as searchActions from '../../store/actions/search.actions';
  * @classdesc Search criteria container.
  */
 export class CriteriaComponent extends AbstractSearchComponent {
+    public resolver: Observable<Resolver>;
+    public resolverIsLoading: Observable<boolean>;
+    public resolverIsLoaded: Observable<boolean>;
+
+    constructor(protected store: Store<{ }>) {
+        super(store);
+        this.resolver = this.store.select(coneSearchSelector.selectResolver);
+        this.resolverIsLoading = this.store.select(coneSearchSelector.selectResolverIsLoading);
+        this.resolverIsLoaded = this.store.select(coneSearchSelector.selectResolverIsLoaded);
+    }
+
     ngOnInit() {
         Promise.resolve(null).then(() => this.store.dispatch(searchActions.changeStep({ step: 'criteria' })));
         Promise.resolve(null).then(() => this.store.dispatch(searchActions.checkCriteria()));
@@ -45,4 +61,16 @@ export class CriteriaComponent extends AbstractSearchComponent {
     deleteCriterion(idCriterion: number): void {
         this.store.dispatch(searchActions.deleteCriterion({ idCriterion }));
     }
+
+    addConeSearch(coneSearch: ConeSearch): void {
+        this.store.dispatch(coneSearchActions.addConeSearch({ coneSearch }));
+    }
+
+    deleteConeSearch(): void {
+        this.store.dispatch(coneSearchActions.deleteConeSearch());
+    }
+
+    retrieveCoordinates(name: string): void {
+        this.store.dispatch(coneSearchActions.retrieveCoordinates({ name }));
+    }
 }
diff --git a/client/src/app/instance/search/containers/dataset.component.html b/client/src/app/instance/search/containers/dataset.component.html
index b86d581556b64900a383598f8beae670d3dff20f..77107988152d7cafa0de1aee2c1d0172768d3afd 100644
--- a/client/src/app/instance/search/containers/dataset.component.html
+++ b/client/src/app/instance/search/containers/dataset.component.html
@@ -36,7 +36,8 @@
                 [outputCategoryList]="outputCategoryList | async"
                 [criteriaList]="criteriaList | async"
                 [outputList]="outputList | async"
-                [queryParams]="queryParams | async">
+                [queryParams]="queryParams | async"
+                [coneSearch]="coneSearch | async">
             </app-summary>
         </div>
     </ng-container>
diff --git a/client/src/app/instance/search/containers/output.component.html b/client/src/app/instance/search/containers/output.component.html
index 9b0cdec4da374ed048fea9eea3630a1628a1e605..12d7f60558376fa5c8b4a679cc8fa1a2b4eafce0 100644
--- a/client/src/app/instance/search/containers/output.component.html
+++ b/client/src/app/instance/search/containers/output.component.html
@@ -27,7 +27,8 @@
             [outputCategoryList]="outputCategoryList | async"
             [criteriaList]="criteriaList | async"
             [outputList]="outputList | async"
-            [queryParams]="queryParams | async">
+            [queryParams]="queryParams | async"
+            [coneSearch]="coneSearch | async">
         </app-summary>
     </div>
 </div>
diff --git a/client/src/app/instance/search/containers/result.component.html b/client/src/app/instance/search/containers/result.component.html
index 9eacc2ffbff8b44a625d444390f47af89e481e08..9e4fec28e05e52c08e8baab91789b911b6bbf5f6 100644
--- a/client/src/app/instance/search/containers/result.component.html
+++ b/client/src/app/instance/search/containers/result.component.html
@@ -36,6 +36,7 @@
                 [outputFamilyList]="outputFamilyList | async"
                 [outputCategoryList]="outputCategoryList | async"
                 [criteriaList]="criteriaList | async"
+                [coneSearch]="coneSearch | async"
                 [outputList]="outputList | async">
             </app-reminder>
             <app-samp
@@ -63,6 +64,7 @@
             </app-cone-search-plot-tab> -->
             <app-datatable-tab
                 [datasetSelected]="datasetSelected | async"
+                [instanceSelected]="instanceSelected | async"
                 [datasetList]="datasetList | async"
                 [attributeList]="attributeList | async"
                 [outputList]="outputList | async"
diff --git a/client/src/app/instance/shared-search/components/cone-search/cone-search.component.html b/client/src/app/instance/shared-search/components/cone-search/cone-search.component.html
index c0cf4330cf393e0886bfdffecd414e6a7720b607..6e676e971e10e1ebf73318e490770eb147abdae1 100644
--- a/client/src/app/instance/shared-search/components/cone-search/cone-search.component.html
+++ b/client/src/app/instance/shared-search/components/cone-search/cone-search.component.html
@@ -1,47 +1,29 @@
 <div class="row pb-4">
     <div class="col">
         <app-resolver
-            [resolverWip]="resolverWip | async"
-            [resolver]="resolver | async"
-            [disabled]="disabled"
-            (resolveName)="retrieveCoordinates($event)">
+            [coneSearch]="coneSearch"
+            [resolver]="resolver"
+            [resolverIsLoading]="resolverIsLoading"
+            [resolverIsLoaded]="resolverIsLoaded"
+            (retrieveCoordinates)="retrieveCoordinates.emit($event)">
         </app-resolver>
     </div>
 </div>
 <div class="row">
     <div class="col pb-4">
-        <app-ra
-            [coneSearch]="coneSearch | async"
-            [resolver]="resolver | async"
-            [unit]="unit"
-            [disabled]="disabled"
-            (updateConeSearch)="updateConeSearch($event)"
-            (deleteResolver)="deleteResolver()">
+        <app-ra [form]="form" [unit]="unit" [resolver]="resolver">
         </app-ra>
     </div>
     <div class="col-auto p-0 align-self-center">
-        <button class="btn btn-outline-secondary"
-            [disabled]="disabled"
-            (click)="unit === 'degree' ? unit = 'hms' : unit = 'degree'"
-            title="Change unit">
-        <span class="fas fa-sync-alt"></span>
+        <button class="btn btn-outline-secondary" [disabled]="coneSearch" (click)="unit === 'degree' ? unit = 'hms' : unit = 'degree'" title="Change unit">
+            <span class="fas fa-sync-alt"></span>
         </button>
     </div>
     <div class="col">
-        <app-dec
-            [coneSearch]="coneSearch | async"
-            [resolver]="resolver | async"
-            [unit]="unit"
-            [disabled]="disabled"
-            (updateConeSearch)="updateConeSearch($event)"
-            (deleteResolver)="deleteResolver()">
+        <app-dec [form]="form" [unit]="unit" [resolver]="resolver">
         </app-dec>
     </div>
     <div class="col-12">
-        <app-radius
-            [coneSearch]="coneSearch | async"
-            [disabled]="disabled"
-            (updateConeSearch)="updateConeSearch($event)">
-        </app-radius>
+        <app-radius [form]="form"></app-radius>
     </div>
 </div>
diff --git a/client/src/app/instance/shared-search/components/cone-search/cone-search.component.ts b/client/src/app/instance/shared-search/components/cone-search/cone-search.component.ts
index d506c3180a782ca2f64141bb863c8e39b23ca25e..ca589c83866668ad1f6fd8953bb7567d637889df 100644
--- a/client/src/app/instance/shared-search/components/cone-search/cone-search.component.ts
+++ b/client/src/app/instance/shared-search/components/cone-search/cone-search.component.ts
@@ -7,9 +7,11 @@
  * file that was distributed with this source code.
  */
 
-import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges } from '@angular/core';
+import { FormGroup, FormControl, Validators } from '@angular/forms';
 
 import { ConeSearch, Resolver } from 'src/app/instance/store/models';
+import { nanValidator, rangeValidator } from '../../validators';
 
 @Component({
     selector: 'app-cone-search',
@@ -19,14 +21,60 @@ import { ConeSearch, Resolver } from 'src/app/instance/store/models';
  * @class
  * @classdesc Cone search container.
  */
-export class ConeSearchComponent {
-    @Input() disabled: boolean = false;
-    @Input() resolverWip: boolean;
-    @Input() resolver: Resolver;
+export class ConeSearchComponent implements OnChanges {
     @Input() coneSearch: ConeSearch;
+    @Input() resolver: Resolver;
+    @Input() resolverIsLoading: boolean;
+    @Input() resolverIsLoaded: boolean;
+    @Output() addConeSearch: EventEmitter<ConeSearch> = new EventEmitter();
+    @Output() deleteConeSearch: EventEmitter<{ }> = new EventEmitter();
     @Output() retrieveCoordinates: EventEmitter<string> = new EventEmitter();
-    @Output() updateConeSearch: EventEmitter<ConeSearch> = new EventEmitter();
-    @Output() deleteResolver: EventEmitter<{}> = new EventEmitter();
 
-    unit = 'degree';
+    public form = new FormGroup({
+        ra: new FormControl('', [Validators.required, nanValidator, rangeValidator(0, 360, 'RA')]),
+        ra_hms: new FormGroup({
+            h: new FormControl('', [Validators.required, nanValidator, rangeValidator(0, 24, 'Hours')]),
+            m: new FormControl('', [Validators.required, nanValidator, rangeValidator(0, 60, 'Minutes')]),
+            s: new FormControl('', [Validators.required, nanValidator, rangeValidator(0, 60, 'Seconds')])
+        }),
+        dec: new FormControl('', [Validators.required, nanValidator, rangeValidator(-90, 90, 'DEC')]),
+        dec_dms: new FormGroup({
+            d: new FormControl('', [Validators.required, nanValidator, rangeValidator(-90, 90, 'Degree')]),
+            m: new FormControl('', [Validators.required, nanValidator, rangeValidator(0, 60, 'Minutes')]),
+            s: new FormControl('', [Validators.required, nanValidator, rangeValidator(0, 60, 'Seconds')])
+        }),
+        radius: new FormControl(2, [Validators.required, rangeValidator(0, 150, 'Radius')])
+    });
+
+    public unit = 'degree';
+
+    ngOnChanges(changes: SimpleChanges) {
+        if (changes.resolver && changes.resolver.currentValue) {
+            this.unit = 'degree';
+        }
+
+        if (changes.coneSearch && !changes.coneSearch.currentValue) {
+            if (this.unit = 'degree') {
+                this.form.controls.ra.enable();
+                this.form.controls.dec.enable();
+                this.form.controls.radius.enable();
+            } else {
+                this.form.controls.ra_hms.enable();
+                this.form.controls.dec_dms.enable();
+            }
+        }
+
+        if (changes.coneSearch && changes.coneSearch.currentValue) {
+            this.form.patchValue(this.coneSearch);
+            this.form.disable();
+        }
+    }
+
+    getConeSearch(): ConeSearch {
+        return {
+            ra: this.form.controls.ra.value,
+            dec: this.form.controls.dec.value,
+            radius: this.form.controls.radius.value
+        };
+    }
 }
diff --git a/client/src/app/instance/shared-search/components/cone-search/dec.component.html b/client/src/app/instance/shared-search/components/cone-search/dec.component.html
index 93539d69f28e7d93341ffa4e254b8440f9acc3aa..c8fe7677d7fa8cafd270f39ecdfae51c2630b970 100644
--- a/client/src/app/instance/shared-search/components/cone-search/dec.component.html
+++ b/client/src/app/instance/shared-search/components/cone-search/dec.component.html
@@ -1,92 +1,75 @@
-<div class="row px-3">
-    <label>DEC</label>
-    <div class="input-group">
-        <input type="text" class="form-control" [formControl]="decDegree" (input)="decChange()" autocomplete="off">
-        <div class="input-group-append">
-            <span class="input-group-text">°</span>
-        </div>
-    </div>
-</div>
-
-<div class="row mt-2 px-3">
-    <div class="col px-0 pr-xl-1">
+<form [formGroup]="form" novalidate>
+    <div class="row px-3">
+        <label>DEC</label>
         <div class="input-group">
-            <input type="text"
-                   class="form-control"
-                   [formControl]="decH"
-                   (input)="decChange()"
-                   (focusin)="changeFocus('dech', true)"
-                   (focusout)="changeFocus('dech', false)"
-                   (change)="setToDefaultValue()"
-                   autocomplete="off">
+            <input type="number" class="form-control" formControlName="dec" autocomplete="off">
             <div class="input-group-append">
                 <span class="input-group-text">°</span>
             </div>
         </div>
     </div>
-    <div class="w-100 d-block d-xl-none"></div>
-    <div class="col mt-1 mt-xl-auto px-0 pr-xl-1">
-        <div class="input-group">
-            <input type="text"
-                   class="form-control"
-                   [formControl]="decM"
-                   (input)="decChange()"
-                   (focusin)="changeFocus('decm', true)"
-                   (focusout)="changeFocus('decm', false)"
-                   (change)="setToDefaultValue()"
-                   autocomplete="off">
-            <div class="input-group-append">
-                <span class="input-group-text">'</span>
+
+    <div formGroupName="dec_dms">
+        <div class="row mt-2 px-3">
+            <div class="col px-0 pr-xl-1">
+                <div class="input-group">
+                    <input type="number" class="form-control" formControlName="d" autocomplete="off">
+                    <div class="input-group-append">
+                        <span class="input-group-text">°</span>
+                    </div>
+                </div>
             </div>
-        </div>
-    </div>
-    <div class="w-100 d-block d-xl-none"></div>
-    <div class="col mt-1 mt-xl-auto px-0">
-        <div class="input-group">
-            <input type="text"
-                   class="form-control"
-                   [formControl]="decS"
-                   (input)="decChange()"
-                   (focusin)="changeFocus('decs', true)"
-                   (focusout)="changeFocus('decs', false)"
-                   (change)="setToDefaultValue()"
-                   autocomplete="off">
-            <div class="input-group-append">
-                <span class="input-group-text">''</span>
+            <div class="w-100 d-block d-xl-none"></div>
+            <div class="col mt-1 mt-xl-auto px-0 pr-xl-1">
+                <div class="input-group">
+                    <input type="number" class="form-control" formControlName="m" autocomplete="off">
+                    <div class="input-group-append">
+                        <span class="input-group-text">'</span>
+                    </div>
+                </div>
+            </div>
+            <div class="w-100 d-block d-xl-none"></div>
+            <div class="col mt-1 mt-xl-auto px-0">
+                <div class="input-group">
+                    <input type="number" class="form-control" formControlName="s" autocomplete="off">
+                    <div class="input-group-append">
+                        <span class="input-group-text">''</span>
+                    </div>
+                </div>
             </div>
         </div>
     </div>
-</div>
+</form>
 
-<div *ngIf="decDegree.invalid" class="row px-3 text-danger">
-    <div *ngIf="decDegree.errors.nan">
-        {{ decDegree.errors.nan.value }}
+<div *ngIf="form.controls.dec.invalid" class="row px-3 text-danger">
+    <div *ngIf="form.controls.dec.errors.nan">
+        {{ form.controls.dec.errors.nan.value }}
     </div>
-    <div *ngIf="decDegree.errors.range" [hidden]="decDegree.errors.nan">
-        {{ decDegree.errors.range.value }}
+    <div *ngIf="form.controls.dec.errors.range" [hidden]="form.controls.dec.errors.nan">
+        {{ form.controls.dec.errors.range.value }}
     </div>
 </div>
-<div *ngIf="decH.invalid" class="row px-3 text-danger">
-    <div *ngIf="decH.errors.nan">
-        {{ decH.errors.nan.value }}
+<div *ngIf="getDecDmsForm().controls.d.invalid" class="row px-3 text-danger">
+    <div *ngIf="getDecDmsForm().controls.d.errors.nan">
+        {{ getDecDmsForm().controls.d.errors.nan.value }}
     </div>
-    <div *ngIf="decH.errors.range" [hidden]="decH.errors.nan">
-        {{ decH.errors.range.value }}
+    <div *ngIf="getDecDmsForm().controls.d.errors.range" [hidden]="getDecDmsForm().controls.d.errors.nan">
+        {{ getDecDmsForm().controls.d.errors.range.value }}
     </div>
 </div>
-<div *ngIf="decM.invalid" class="row px-3 text-danger">
-    <div *ngIf="decM.errors.nan">
-        {{ decM.errors.nan.value }}
+<div *ngIf="getDecDmsForm().controls.m.invalid" class="row px-3 text-danger">
+    <div *ngIf="getDecDmsForm().controls.m.errors.nan">
+        {{ getDecDmsForm().controls.m.errors.nan.value }}
     </div>
-    <div *ngIf="decM.errors.range" [hidden]="decM.errors.nan">
-        {{ decM.errors.range.value }}
+    <div *ngIf="getDecDmsForm().controls.m.errors.range" [hidden]="getDecDmsForm().controls.m.errors.nan">
+        {{ getDecDmsForm().controls.m.errors.range.value }}
     </div>
 </div>
-<div *ngIf="decS.invalid" class="row px-3 text-danger">
-    <div *ngIf="decS.errors.nan">
-        {{ decS.errors.nan.value }}
+<div *ngIf="getDecDmsForm().controls.s.invalid" class="row px-3 text-danger">
+    <div *ngIf="getDecDmsForm().controls.s.errors.nan">
+        {{ getDecDmsForm().controls.s.errors.nan.value }}
     </div>
-    <div *ngIf="decS.errors.range" [hidden]="decS.errors.nan">
-        {{ decS.errors.range.value }}
+    <div *ngIf="getDecDmsForm().controls.s.errors.range" [hidden]="getDecDmsForm().controls.s.errors.nan">
+        {{ getDecDmsForm().controls.s.errors.range.value }}
     </div>
 </div>
diff --git a/client/src/app/instance/shared-search/components/cone-search/dec.component.ts b/client/src/app/instance/shared-search/components/cone-search/dec.component.ts
index 3566031548cf7ad568b692487382f20033bccb0d..193bc66543fb5e9e5110c7d582591d5305160e29 100644
--- a/client/src/app/instance/shared-search/components/cone-search/dec.component.ts
+++ b/client/src/app/instance/shared-search/components/cone-search/dec.component.ts
@@ -7,11 +7,13 @@
  * file that was distributed with this source code.
  */
 
-import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
-import { FormControl, Validators } from '@angular/forms';
+import { Component, Input, ChangeDetectionStrategy, OnInit, OnDestroy, OnChanges, SimpleChanges } from '@angular/core';
+import { FormGroup } from '@angular/forms';
 
-import { nanValidator, rangeValidator } from '../../validators';
-import { ConeSearch, Resolver } from 'src/app/instance/store/models';
+import { Subscription } from 'rxjs';
+import { debounceTime } from 'rxjs/operators';
+
+import { Resolver } from 'src/app/instance/store/models';
 
 @Component({
     selector: 'app-dec',
@@ -23,199 +25,71 @@ import { ConeSearch, Resolver } from 'src/app/instance/store/models';
  * @class
  * @classdesc DEC component.
  */
-export class DecComponent {
-    /**
-     * Disables DEC fields.
-     *
-     * @param  {boolean} disabled - If the field has to be disabled.
-     */
-    @Input()
-    set disabled(disabled: boolean) {
-        this.isDisabled = disabled;
-        this.initFields();
+export class DecComponent implements OnInit, OnDestroy, OnChanges {
+    @Input() form: FormGroup;
+    @Input() unit: string;
+    @Input() resolver: Resolver;
+
+    public decControlSubscription: Subscription
+    public decDmsSubscription: Subscription;
+
+    ngOnInit() {
+        this.form.controls.dec_dms.disable();
+        this.decControlSubscription = this.form.controls.dec.valueChanges.pipe(debounceTime(250))
+            .subscribe(deg => this.deg2DMS(deg));
     }
-    /**
-     * Sets RA, DEC and radius from cone search.
-     *
-     * @param  {ConeSearch} coneSearch - The cone search.
-     */
-    @Input()
-    set coneSearch(coneSearch: ConeSearch) {
-        this.ra = coneSearch.ra;
-        this.radius = coneSearch.radius;
-        if (coneSearch.dec) {
-            this.decDegree.setValue(coneSearch.dec);
-            if(this.decDegree.valid && !this.decHFocused && !this.decMFocused && !this.decSFocused) {
-                this.decDegree2HMS(coneSearch.dec);
+
+    ngOnChanges(changes: SimpleChanges): void {
+        if (changes.unit && !changes.unit.firstChange) {
+            if (changes.unit.currentValue === 'degree') {
+                this.form.controls.dec_dms.disable();
+                this.form.controls.dec.enable();
+                this.decControlSubscription = this.form.controls.dec.valueChanges.pipe(debounceTime(250))
+                    .subscribe(deg => this.deg2DMS(deg));
+                if (this.decDmsSubscription) this.decDmsSubscription.unsubscribe();
+            }
+            if (changes.unit.currentValue === 'hms') {
+                this.form.controls.dec_dms.enable();
+                this.form.controls.dec.disable();
+                this.decDmsSubscription = this.form.controls.dec_dms.valueChanges.pipe(debounceTime(250))
+                    .subscribe(value => this.DMS2Deg(value));
+                if (this.decControlSubscription) this.decControlSubscription.unsubscribe();
             }
-        } else {
-            this.decDegree.reset();
-            this.decH.reset();
-            this.decM.reset();
-            this.decS.reset();
         }
-        this.initFields();
-    }
-    /**
-     * Sets RA from resolver.
-     *
-     * @param  {Resolver} resolver - The resolver.
-     */
-    @Input()
-    set resolver(resolver: Resolver) {
-        this.resolvedDec = null;
-        if (resolver) {
-            this.resolvedDec = resolver.dec;
-            this.decDegree.setValue(resolver.dec);
-            this.decDegree2HMS(resolver.dec);
+
+        if (changes.resolver && changes.resolver.currentValue) {
+            this.form.controls.dec.setValue(changes.resolver.currentValue.dec);
         }
     }
-    /**
-     * Sets isDegree.
-     *
-     * @param  {string} unit - The unit.
-     */
-    @Input()
-    set unit(unit: string) {
-        unit === 'degree' ? this.isDegree = true : this.isDegree = false;
-        this.initFields();
-    }
-    @Output() updateConeSearch: EventEmitter<ConeSearch> = new EventEmitter();
-    @Output() deleteResolver: EventEmitter<null> = new EventEmitter();
-
-    ra: number;
-    radius: number;
-    isDisabled = false;
-    isDegree = true;
-    resolvedDec: number;
-    decHFocused: boolean = false;
-    decMFocused: boolean = false;
-    decSFocused: boolean = false;
-
-    decDegree = new FormControl('', [Validators.required, nanValidator, rangeValidator(-90, 90, 'DEC')]);
-    decH = new FormControl('', [nanValidator, rangeValidator(-90, 90, 'Degree')]);
-    decM = new FormControl('', [nanValidator, rangeValidator(0, 60, 'Minutes')]);
-    decS = new FormControl('', [nanValidator, rangeValidator(0, 60, 'Seconds')]);
 
-    /**
-     * Sets DEC fields.
-     */
-    initFields(): void {
-        if (this.isDisabled) {
-            this.decDegree.disable();
-            this.decH.disable();
-            this.decM.disable();
-            this.decS.disable();
-        } else if (this.isDegree) {
-            this.decDegree.enable();
-            this.decH.disable();
-            this.decM.disable();
-            this.decS.disable();
-        } else {
-            this.decDegree.disable();
-            this.decH.enable();
-            this.decM.enable();
-            this.decS.enable();
-        }
+    getDecDmsForm() {
+        const decDmsForm = this.form.controls.dec_dms as FormGroup;
+        return decDmsForm;
     }
 
-    /**
-     * Converts DEC hour minute second from degree and sets DEC HMS fields.
-     *
-     * @param  {number} value - The degree value.
-     */
-    decDegree2HMS(value: number): void {
-        const hh = Math.trunc(value);
-        let tmp = (Math.abs(value - hh)) * 60;
+    deg2DMS(deg: number): void {
+        const hh = Math.trunc(deg);
+        let tmp = (Math.abs(deg - hh)) * 60;
         const mm = Math.trunc(tmp);
         tmp = (tmp - mm) * 60;
         const ss = tmp.toFixed(2);
-        this.decH.setValue(hh);
-        this.decM.setValue(mm);
-        this.decS.setValue(ss);
+        const decDmsForm = this.getDecDmsForm();
+        decDmsForm.controls.d.setValue(hh);
+        decDmsForm.controls.m.setValue(mm);
+        decDmsForm.controls.s.setValue(ss);
     }
 
-    /**
-     * Sets DEC degree from hour minute second and sets DEC degree field.
-     */
-    decHMS2Degree(): void {
-        const hh = +this.decH.value;
-        const mm = +this.decM.value;
-        const ss = +this.decS.value;
-        const tmp = ((ss / 60) + mm) / 60;
-        let deg = tmp + Math.abs(hh);
-        if (hh < 0) {
+    DMS2Deg(dms: {d: number, m: number, s: number }): void {
+        const tmp = ((dms.s / 60) + dms.m) / 60;
+        let deg = tmp + Math.abs(dms.d);
+        if (dms.d < 0) {
             deg = -deg;
         }
-        this.decDegree.setValue(+deg.toFixed(8));
+        this.form.controls.dec.setValue(deg);
     }
 
-    /**
-     * Changes fields focus.
-     *
-     * @param  {string} field - The field.
-     * @param  {boolean} isFocused - Is the field is focused.
-     */
-    changeFocus(field: string, isFocused: boolean) {
-        switch (field) {
-            case 'dech':
-                this.decHFocused = isFocused;
-                break;
-            case 'decm':
-                this.decMFocused = isFocused;
-                break
-            case 'decs':
-                this.decSFocused = isFocused;
-                break;
-        }
-    }
-
-    /**
-     * Manages DEC value change.
-     */
-    decChange(): void {
-        if (this.isDegree) {
-            if (this.decDegree.valid) {
-                this.decDegree2HMS(this.decDegree.value);
-            } else {
-                this.decH.reset();
-                this.decM.reset();
-                this.decS.reset();
-            }
-            this.updateConeSearch.emit({ ra: this.ra, dec: this.decDegree.value, radius: this.radius } as ConeSearch);
-        } else {
-            if (this.decH.valid && this.decM.valid && this.decS.valid) {
-                this.setToDefaultValue();
-                this.decHMS2Degree();
-                this.updateConeSearch.emit({ ra: this.ra, dec: this.decDegree.value, radius: this.radius } as ConeSearch);
-            } else {
-                this.decDegree.reset();
-            }
-        }
-        this.resetResolver();
-    }
-
-    /**
-     * Sets DEC hour minute second fields to default value if not valid.
-     */
-    setToDefaultValue(): void {
-        if (this.decH.value === '' || this.decH.value === null) {
-            this.decH.setValue(0);
-        }
-        if (this.decM.value === '' || this.decM.value === null) {
-            this.decM.setValue(0);
-        }
-        if (this.decS.value === '' || this.decS.value === null) {
-            this.decS.setValue(0);
-        }
-    }
-
-    /**
-     * Emits reset resolver event.
-     */
-    resetResolver(): void {
-        if (this.resolvedDec && this.resolvedDec !== this.decDegree.value) {
-            this.deleteResolver.emit();
-        }
+    ngOnDestroy() {
+        if (this.decControlSubscription) this.decControlSubscription.unsubscribe();
+        if (this.decDmsSubscription) this.decDmsSubscription.unsubscribe();
     }
 }
diff --git a/client/src/app/instance/shared-search/components/cone-search/ra.component.html b/client/src/app/instance/shared-search/components/cone-search/ra.component.html
index e1ff92f6fa13d6a68cb3cbf742663ba56fe49b94..bb180bf81ba5dc29c24c2197b74691d51fbd7002 100644
--- a/client/src/app/instance/shared-search/components/cone-search/ra.component.html
+++ b/client/src/app/instance/shared-search/components/cone-search/ra.component.html
@@ -1,92 +1,75 @@
-<div class="row px-3">
-    <label>RA</label>
-    <div class="input-group">
-        <input type="text" class="form-control" [formControl]="raDegree" (input)="raChange()" autocomplete="off">
-        <div class="input-group-append">
-            <span class="input-group-text">°</span>
-        </div>
-    </div>
-</div>
-
-<div class="row mt-2 px-3">
-    <div class="col px-0 pr-xl-1">
+<form [formGroup]="form" novalidate>
+    <div class="row px-3">
+        <label>RA</label>
         <div class="input-group">
-            <input type="text"
-                   class="form-control"
-                   [formControl]="raH"
-                   (input)="raChange()"
-                   (focusin)="changeFocus('rah', true)"
-                   (focusout)="changeFocus('rah', false)"
-                   (change)="setToDefaultValue()"
-                   autocomplete="off">
+            <input type="number" class="form-control" formControlName="ra" autocomplete="off">
             <div class="input-group-append">
-                <span class="input-group-text">H</span>
+                <span class="input-group-text">°</span>
             </div>
         </div>
     </div>
-    <div class="w-100 d-block d-xl-none"></div>
-    <div class="col mt-1 mt-xl-auto px-0 pr-xl-1">
-        <div class="input-group">
-            <input type="text"
-                   class="form-control"
-                   [formControl]="raM"
-                   (input)="raChange()"
-                   (focusin)="changeFocus('ram', true)"
-                   (focusout)="changeFocus('ram', false)"
-                   (change)="setToDefaultValue()"
-                   autocomplete="off">
-            <div class="input-group-append">
-                <span class="input-group-text">'</span>
+
+    <div formGroupName="ra_hms">
+        <div class="row mt-2 px-3">
+            <div class="col px-0 pr-xl-1">
+                <div class="input-group">
+                    <input type="number" class="form-control" formControlName="h" autocomplete="off">
+                    <div class="input-group-append">
+                        <span class="input-group-text">H</span>
+                    </div>
+                </div>
             </div>
-        </div>
-    </div>
-    <div class="w-100 d-block d-xl-none"></div>
-    <div class="col mt-1 mt-xl-auto px-0">
-        <div class="input-group">
-            <input type="text"
-                   class="form-control"
-                   [formControl]="raS"
-                   (input)="raChange()"
-                   (focusin)="changeFocus('ras', true)"
-                   (focusout)="changeFocus('ras', false)"
-                   (change)="setToDefaultValue()"
-                   autocomplete="off">
-            <div class="input-group-append">
-                <span class="input-group-text">''</span>
+            <div class="w-100 d-block d-xl-none"></div>
+            <div class="col mt-1 mt-xl-auto px-0 pr-xl-1">
+                <div class="input-group">
+                    <input type="number" class="form-control" formControlName="m" autocomplete="off">
+                    <div class="input-group-append">
+                        <span class="input-group-text">'</span>
+                    </div>
+                </div>
+            </div>
+            <div class="w-100 d-block d-xl-none"></div>
+            <div class="col mt-1 mt-xl-auto px-0">
+                <div class="input-group">
+                    <input type="number" class="form-control" formControlName="s" autocomplete="off">
+                    <div class="input-group-append">
+                        <span class="input-group-text">''</span>
+                    </div>
+                </div>
             </div>
         </div>
     </div>
-</div>
+</form>
 
-<div *ngIf="raDegree.invalid" class="row px-3 text-danger">
-    <div *ngIf="raDegree.errors.nan">
-        {{ raDegree.errors.nan.value }}
+<div *ngIf="form.controls.ra.invalid" class="row px-3 text-danger">
+    <div *ngIf="form.controls.ra.errors.nan">
+        {{ form.controls.ra.errors.nan.value }}
     </div>
-    <div *ngIf="raDegree.errors.range" [hidden]="raDegree.errors.nan">
-        {{ raDegree.errors.range.value }}
+    <div *ngIf="form.controls.ra.errors.range" [hidden]="form.controls.ra.errors.nan">
+        {{ form.controls.ra.errors.range.value }}
     </div>
 </div>
-<div *ngIf="raH.invalid" class="row px-3 text-danger">
-    <div *ngIf="raH.errors.nan">
-        {{ raH.errors.nan.value }}
+<div *ngIf="getRaHmsForm().controls.h.invalid" class="row px-3 text-danger">
+    <div *ngIf="getRaHmsForm().controls.h.errors.nan">
+        {{ getRaHmsForm().controls.h.errors.nan.value }}
     </div>
-    <div *ngIf="raH.errors.range" [hidden]="raH.errors.nan">
-        {{ raH.errors.range.value }}
+    <div *ngIf="getRaHmsForm().controls.h.errors.range" [hidden]="getRaHmsForm().controls.h.errors.nan">
+        {{ getRaHmsForm().controls.h.errors.range.value }}
     </div>
 </div>
-<div *ngIf="raM.invalid" class="row px-3 text-danger">
-    <div *ngIf="raM.errors.nan">
-        {{ raM.errors.nan.value }}
+<div *ngIf="getRaHmsForm().controls.m.invalid" class="row px-3 text-danger">
+    <div *ngIf="getRaHmsForm().controls.m.errors.nan">
+        {{ getRaHmsForm().controls.m.errors.nan.value }}
     </div>
-    <div *ngIf="raM.errors.range" [hidden]="raM.errors.nan">
-        {{ raM.errors.range.value }}
+    <div *ngIf="getRaHmsForm().controls.m.errors.range" [hidden]="getRaHmsForm().controls.m.errors.nan">
+        {{ getRaHmsForm().controls.m.errors.range.value }}
     </div>
 </div>
-<div *ngIf="raS.invalid" class="row px-3 text-danger">
-    <div *ngIf="raS.errors.nan">
-        {{ raS.errors.nan.value }}
+<div *ngIf="getRaHmsForm().controls.s.invalid" class="row px-3 text-danger">
+    <div *ngIf="getRaHmsForm().controls.s.errors.nan">
+        {{ getRaHmsForm().controls.s.errors.nan.value }}
     </div>
-    <div *ngIf="raS.errors.range" [hidden]="raS.errors.nan">
-        {{ raS.errors.range.value }}
+    <div *ngIf="getRaHmsForm().controls.s.errors.range" [hidden]="getRaHmsForm().controls.s.errors.nan">
+        {{ getRaHmsForm().controls.s.errors.range.value }}
     </div>
 </div>
diff --git a/client/src/app/instance/shared-search/components/cone-search/ra.component.ts b/client/src/app/instance/shared-search/components/cone-search/ra.component.ts
index 30050f202540cf03c3d6659588e2c2c1595b5e90..aab36bf7ec811d5984d1d977b0b78847db203ab9 100644
--- a/client/src/app/instance/shared-search/components/cone-search/ra.component.ts
+++ b/client/src/app/instance/shared-search/components/cone-search/ra.component.ts
@@ -7,11 +7,13 @@
  * file that was distributed with this source code.
  */
 
-import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
-import { FormControl, Validators } from '@angular/forms';
+import { Component, Input, ChangeDetectionStrategy, OnDestroy, OnInit, OnChanges, SimpleChanges } from '@angular/core';
+import { FormGroup } from '@angular/forms';
 
-import { nanValidator, rangeValidator } from '../../validators';
-import { ConeSearch, Resolver } from 'src/app/instance/store/models';
+import { Subscription } from 'rxjs';
+import { debounceTime } from 'rxjs/operators';
+
+import { Resolver } from 'src/app/instance/store/models';
 
 @Component({
     selector: 'app-ra',
@@ -23,196 +25,68 @@ import { ConeSearch, Resolver } from 'src/app/instance/store/models';
  * @class
  * @classdesc RA component.
  */
-export class RaComponent {
-    /**
-     * Disables RA fields.
-     *
-     * @param  {boolean} disabled - If the field has to be disabled.
-     */
-    @Input()
-    set disabled(disabled: boolean) {
-        this.isDisabled = disabled;
-        this.initFields();
+export class RaComponent implements OnInit, OnDestroy, OnChanges  {
+    @Input() form: FormGroup;
+    @Input() unit: string;
+    @Input() resolver: Resolver;
+
+    public raControlSubscription: Subscription;
+    public raHmsFormSubscription: Subscription;
+
+    ngOnInit() {
+        this.form.controls.ra_hms.disable();
+        this.raControlSubscription = this.form.controls.ra.valueChanges.pipe(debounceTime(250))
+            .subscribe(deg => this.deg2HMS(deg));
     }
-    /**
-     * Sets RA, DEC and radius from cone search.
-     *
-     * @param  {ConeSearch} coneSearch - The cone search.
-     */
-    @Input()
-    set coneSearch(coneSearch: ConeSearch) {
-        this.dec = coneSearch.dec;
-        this.radius = coneSearch.radius;
-        if (coneSearch.ra) {
-            this.raDegree.setValue(coneSearch.ra);
-            if (this.raDegree.valid && !this.raHFocused && !this.raMFocused && !this.raSFocused) {
-                this.raDegree2HMS(coneSearch.ra);
+
+    ngOnChanges(changes: SimpleChanges): void {
+        if (changes.unit && !changes.unit.firstChange) {
+            if (changes.unit.currentValue === 'degree') {
+                this.form.controls.ra_hms.disable();
+                this.form.controls.ra.enable();
+                this.raControlSubscription = this.form.controls.ra.valueChanges.pipe(debounceTime(250))
+                    .subscribe(deg => this.deg2HMS(deg));
+                if (this.raHmsFormSubscription) this.raHmsFormSubscription.unsubscribe();
+            }
+            if (changes.unit.currentValue === 'hms') {
+                this.form.controls.ra_hms.enable();
+                this.form.controls.ra.disable();
+                this.raHmsFormSubscription = this.form.controls.ra_hms.valueChanges.pipe(debounceTime(250))
+                    .subscribe(value => this.HMS2Deg(value));
+                if (this.raControlSubscription) this.raControlSubscription.unsubscribe();
             }
-        } else {
-            this.raDegree.reset();
-            this.raH.reset();
-            this.raM.reset();
-            this.raS.reset();
         }
-        this.initFields();
-    }
-    /**
-     * Sets RA from resolver.
-     *
-     * @param  {Resolver} resolver - The resolver.
-     */
-    @Input()
-    set resolver(resolver: Resolver) {
-        this.resolvedRa = null;
-        if (resolver) {
-            this.resolvedRa = resolver.ra;
-            this.raDegree.setValue(resolver.ra);
-            this.raDegree2HMS(resolver.ra);
+
+        if (changes.resolver && changes.resolver.currentValue) {
+            this.form.controls.ra.setValue(changes.resolver.currentValue.ra);
         }
     }
-    /**
-     * Sets isDegree.
-     *
-     * @param  {string} unit - The unit.
-     */
-    @Input()
-    set unit(unit: string) {
-        unit === 'degree' ? this.isDegree = true : this.isDegree = false;
-        this.initFields();
-    }
-    @Output() updateConeSearch: EventEmitter<ConeSearch> = new EventEmitter();
-    @Output() deleteResolver: EventEmitter<null> = new EventEmitter();
-
-    dec: number = null;
-    radius: number = null;
-    isDisabled = false;
-    isDegree = true;
-    resolvedRa: number;
-    raHFocused: boolean = false;
-    raMFocused: boolean = false;
-    raSFocused: boolean = false;
-
-    raDegree = new FormControl('', [Validators.required, nanValidator, rangeValidator(0, 360, 'RA')]);
-    raH = new FormControl('', [nanValidator, rangeValidator(0, 24, 'Hours')]);
-    raM = new FormControl('', [nanValidator, rangeValidator(0, 60, 'Minutes')]);
-    raS = new FormControl('', [nanValidator, rangeValidator(0, 60, 'Seconds')]);
 
-    /**
-     * Sets RA fields.
-     */
-    initFields(): void {
-        if (this.isDisabled) {
-            this.raDegree.disable();
-            this.raH.disable();
-            this.raM.disable();
-            this.raS.disable();
-        } else if (this.isDegree) {
-            this.raDegree.enable();
-            this.raH.disable();
-            this.raM.disable();
-            this.raS.disable();
-        } else {
-            this.raDegree.disable();
-            this.raH.enable();
-            this.raM.enable();
-            this.raS.enable();
-        }
+    getRaHmsForm() {
+        const raHmsForm = this.form.controls.ra_hms as FormGroup;
+        return raHmsForm;
     }
 
-    /**
-     * Converts RA hour minute second from degree and sets RA HMS fields.
-     *
-     * @param  {number} value - The degree value.
-     */
-    raDegree2HMS(value: number): void {
-        let tmp = value / 15;
+    deg2HMS(deg: number): void {
+        let tmp = deg / 15;
         const hh = Math.trunc(tmp);
         tmp = (tmp - hh) * 60;
         const mm = Math.trunc(tmp);
         tmp = (tmp - mm) * 60;
         const ss = +tmp.toFixed(2);
-        this.raH.setValue(hh);
-        this.raM.setValue(mm);
-        this.raS.setValue(ss);
+        const raHmsForm = this.getRaHmsForm();
+        raHmsForm.controls.h.setValue(hh);
+        raHmsForm.controls.m.setValue(mm);
+        raHmsForm.controls.s.setValue(ss);
     }
 
-    /**
-     * Sets RA degree from hour minute second and sets RA degree field.
-     */
-    raHMS2Degree(): void {
-        const hh = +this.raH.value;
-        const mm = +this.raM.value;
-        const ss = +this.raS.value;
-        const deg = +(((((ss / 60) + mm) / 60) + hh) * 15).toFixed(8);
-        this.raDegree.setValue(deg);
+    HMS2Deg(hms: {h: number, m: number, s: number }): void {
+        const deg = +(((((hms.s / 60) + hms.m) / 60) + hms.h) * 15).toFixed(8);
+        this.form.controls.ra.setValue(deg);
     }
 
-    /**
-     * Changes fields focus.
-     *
-     * @param  {string} field - The field.
-     * @param  {boolean} isFocused - Is the field is focused.
-     */
-    changeFocus(field: string, isFocused: boolean): void {
-        switch (field) {
-            case 'rah':
-                this.raHFocused = isFocused;
-                break;
-            case 'ram':
-                this.raMFocused = isFocused;
-                break
-            case 'ras':
-                this.raSFocused = isFocused;
-                break;
-        }
-    }
-
-    /**
-     * Manages RA value change.
-     */
-    raChange(): void {
-        if (this.isDegree) {
-            if (this.raDegree.valid) {
-                this.raDegree2HMS(this.raDegree.value);
-            } else {
-                this.raH.reset();
-                this.raM.reset();
-                this.raS.reset();
-            }
-            this.updateConeSearch.emit({ ra: this.raDegree.value, dec: this.dec, radius: this.radius } as ConeSearch);
-        } else {
-            if (this.raH.valid && this.raM.valid && this.raS.valid) {
-                this.setToDefaultValue();
-                this.raHMS2Degree();
-                this.updateConeSearch.emit({ ra: this.raDegree.value, dec: this.dec, radius: this.radius } as ConeSearch);
-            } else {
-                this.raDegree.reset();
-            }
-        }
-        this.resetResolver();
-    }
-
-    /**
-     * Sets RA hour minute second fields to default value if not valid.
-     */
-    setToDefaultValue(): void {
-        if (this.raH.value === '' || this.raH.value === null) {
-            this.raH.setValue(0);
-        }
-        if (this.raM.value === '' || this.raM.value === null) {
-            this.raM.setValue(0);
-        }
-        if (this.raS.value === '' || this.raS.value === null) {
-            this.raS.setValue(0);
-        }
-    }
-
-    /**
-     * Emits reset resolver event.
-     */
-    resetResolver(): void {
-        if (this.resolvedRa && this.resolvedRa !== this.raDegree.value) {
-            this.deleteResolver.emit();
-        }
+    ngOnDestroy() {
+        if (this.raControlSubscription) this.raControlSubscription.unsubscribe();
+        if (this.raHmsFormSubscription) this.raHmsFormSubscription.unsubscribe();
     }
 }
diff --git a/client/src/app/instance/shared-search/components/cone-search/radius.component.html b/client/src/app/instance/shared-search/components/cone-search/radius.component.html
index 406cdfc30e2dcaeb8151aad6ac776a9d23cc3c60..87cdf7a216c9304bef8e199db31a26860a70adda 100644
--- a/client/src/app/instance/shared-search/components/cone-search/radius.component.html
+++ b/client/src/app/instance/shared-search/components/cone-search/radius.component.html
@@ -1,27 +1,22 @@
-<div class="row">
-    <div class="col form-group mb-0">
-        <label>Radius</label>
-        <input #rr
-            type="range"
-            min="0"
-            max="150"
-            [formControl]="radiusRange"
-            (input)="radiusChange(rr.value)"
-            class="form-control-range mt-2"
-            autocomplete="off">
-    </div>
-    <div class="w-100 d-block d-lg-none"></div>
-    <div class="col col-lg-auto form-group mb-0">
-        <div class="input-group mt-4">
-            <input #rf id="radius-field" type="number" class="form-control" [formControl]="radiusField" (input)="radiusChange(rf.value)" autocomplete="off">
-            <div class="input-group-append">
-                <span class="input-group-text">arcsecond</span>
+<form [formGroup]="form" novalidate>
+    <div class="row">
+        <div class="col form-group mb-0">
+            <label>Radius</label>
+            <input type="range" min="0" max="150" class="form-control-range mt-2" [value]="form.value.radius" formControlName="radius" autocomplete="off">
+        </div>
+        <div class="w-100 d-block d-lg-none"></div>
+        <div class="col col-lg-auto form-group mb-0">
+            <div class="input-group mt-4">
+                <input type="number" class="form-control" [value]="form.value.radius" formControlName="radius" autocomplete="off">
+                <div class="input-group-append">
+                    <span class="input-group-text">arcsecond</span>
+                </div>
             </div>
         </div>
-    </div>
-    <div *ngIf="radiusField.invalid" class="col-12 text-danger">
-        <div *ngIf="radiusField.errors.range">
-            {{ radiusField.errors.range.value }}
+        <div *ngIf="form.controls.radius.invalid" class="col-12 text-danger">
+            <div *ngIf="form.controls.radius.errors.range">
+                {{ form.controls.radius.errors.range.value }}
+            </div>
         </div>
     </div>
-</div>
+</form>
\ No newline at end of file
diff --git a/client/src/app/instance/shared-search/components/cone-search/radius.component.ts b/client/src/app/instance/shared-search/components/cone-search/radius.component.ts
index 5add3abf531de6ea605e5547a42bc19832ec16f1..fde6ceb216655a19b21d8966877a01fd29a0d0b1 100644
--- a/client/src/app/instance/shared-search/components/cone-search/radius.component.ts
+++ b/client/src/app/instance/shared-search/components/cone-search/radius.component.ts
@@ -7,11 +7,8 @@
  * file that was distributed with this source code.
  */
 
-import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
-import { FormControl } from '@angular/forms';
-
-import { rangeValidator } from '../../validators';
-import { ConeSearch } from 'src/app/instance/store/models';
+import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
+import { FormGroup } from '@angular/forms';
 
 @Component({
     selector: 'app-radius',
@@ -24,55 +21,5 @@ import { ConeSearch } from 'src/app/instance/store/models';
  * @classdesc Radius component.
  */
 export class RadiusComponent {
-    /**
-     * Sets RA, DEC and radius from cone search.
-     *
-     * @param  {ConeSearch} coneSearch - The cone search.
-     */
-    @Input()
-    set coneSearch(coneSearch: ConeSearch) {
-        this.ra = coneSearch.ra;
-        this.dec = coneSearch.dec;
-        if (coneSearch.radius) {
-            this.radiusField.setValue(coneSearch.radius);
-            this.radiusRange.setValue(coneSearch.radius);
-        } else {
-            this.radiusRange.setValue(0);
-            this.radiusField.setValue(0);
-        }
-    }
-    /**
-     * Disables radius fields.
-     *
-     * @param  {boolean} disabled - If the field has to be disabled.
-     */
-    @Input()
-    set disabled(disabled: boolean) {
-        if (disabled) {
-            this.radiusField.disable();
-            this.radiusRange.disable();
-        } else {
-            this.radiusField.enable();
-            this.radiusRange.enable();
-        }
-    }
-    @Output() updateConeSearch: EventEmitter<ConeSearch> = new EventEmitter();
-
-    ra: number;
-    dec: number;
-    radiusRange = new FormControl('');
-    radiusField = new FormControl('', [rangeValidator(0, 150, 'Radius')]);
-
-    /**
-     * Sets radius value form inputs and emits cone search event.
-     *
-     * @param  {string} value - The value of radius.
-     *
-     * @fires EventEmitter<ConeSearch>
-     */
-    radiusChange(value: string): void {
-        this.radiusField.setValue(+value);
-        this.radiusRange.setValue(+value);
-        this.updateConeSearch.emit({ ra: this.ra, dec: this.dec, radius: +value } as ConeSearch);
-    }
+    @Input() form: FormGroup;
 }
diff --git a/client/src/app/instance/shared-search/components/cone-search/resolver.component.html b/client/src/app/instance/shared-search/components/cone-search/resolver.component.html
index 86714689d9db49082707206b9002ed94cdf661e8..abf2a55cc7b6b98bc988778340658be4d0588d4b 100644
--- a/client/src/app/instance/shared-search/components/cone-search/resolver.component.html
+++ b/client/src/app/instance/shared-search/components/cone-search/resolver.component.html
@@ -1,15 +1,17 @@
-<div class="row">
-    <div class="col pr-0">
-        <label for="resolver">Resolve RA and DEC with Sesame Name Resolver</label>
-        <input #n id="resolver" type="text" class="form-control" [formControl]="field" autocomplete="off">
+<form [formGroup]="form" (ngSubmit)="submit()" novalidate>
+    <div class="row">
+        <div class="col pr-0">
+            <label for="resolver">Resolve RA and DEC with Sesame Name Resolver</label>
+            <input type="text" class="form-control" name="name" formControlName="name" autocomplete="off">
+        </div>
+        <div class="col-auto pt-5 pt-lg-4">
+            <button *ngIf="!resolverIsLoading" [disabled]="!form.valid || form.pristine" type="submit" class="btn btn-outline-secondary mt-2">
+                <span class="fas fa-search"></span>
+            </button>
+            <button *ngIf="resolverIsLoading" class="btn btn-outline-secondary mt-2" disabled>
+                <span class="fas fa-circle-notch fa-spin"></span>
+                <span class="sr-only">Loading...</span>
+            </button>
+        </div>
     </div>
-    <div class="col-auto pt-5 pt-lg-4">
-        <button *ngIf="!resolverWip" id="btn-search" class="btn btn-outline-secondary mt-2" [disabled]="field.disabled" (click)="resolveName.emit(n.value)">
-            <span class="fas fa-search"></span>
-        </button>
-        <button *ngIf="resolverWip" id="btn-wip" class="btn btn-outline-secondary mt-2" disabled>
-            <span class="fas fa-circle-notch fa-spin"></span>
-            <span class="sr-only">Loading...</span>
-        </button>
-    </div>
-</div>
+</form>
diff --git a/client/src/app/instance/shared-search/components/cone-search/resolver.component.ts b/client/src/app/instance/shared-search/components/cone-search/resolver.component.ts
index 1c01755596db0bfad2c8509903f7f76d68e1f957..18c094444753d9795a7088eb0aca5e3a1a8fae1d 100644
--- a/client/src/app/instance/shared-search/components/cone-search/resolver.component.ts
+++ b/client/src/app/instance/shared-search/components/cone-search/resolver.component.ts
@@ -7,10 +7,10 @@
  * file that was distributed with this source code.
  */
 
-import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
-import { FormControl } from '@angular/forms';
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit, OnChanges, SimpleChanges } from '@angular/core';
+import { FormGroup, FormControl, Validators } from '@angular/forms';
 
-import { Resolver } from 'src/app/instance/store/models';
+import { ConeSearch, Resolver } from 'src/app/instance/store/models';
 
 @Component({
     selector: 'app-resolver',
@@ -21,25 +21,34 @@ import { Resolver } from 'src/app/instance/store/models';
  * @class
  * @classdesc Resolver component.
  */
-export class ResolverComponent {
-    @Input()
-    set disabled(disabled: boolean) {
-        if (disabled) {
-            this.field.disable();
-        } else {
-            this.field.enable();
+export class ResolverComponent implements OnInit, OnChanges {
+    @Input() coneSearch: ConeSearch;
+    @Input() resolver: Resolver;
+    @Input() resolverIsLoading: boolean;
+    @Input() resolverIsLoaded: boolean;
+    @Output() retrieveCoordinates: EventEmitter<string> = new EventEmitter();
+
+    public form = new FormGroup({
+        name: new FormControl('', [Validators.required])
+    });
+
+    ngOnInit() {
+        if (this.coneSearch) {
+            this.form.disable();
         }
     }
-    @Input() resolverWip: boolean;
-    @Input()
-    set resolver(resolver: Resolver) {
-        if (resolver) {
-            this.field.setValue(resolver.name);
-        } else {
-            this.field.reset();
+
+    ngOnChanges(changes: SimpleChanges): void {
+        if (changes.coneSearch && !changes.coneSearch.currentValue) {
+            this.form.enable();
+        }
+
+        if (changes.coneSearch && changes.coneSearch.currentValue) {
+            this.form.disable();
         }
     }
-    @Output() resolveName: EventEmitter<string> = new EventEmitter();
 
-    field = new FormControl('');
+    submit() {
+        this.retrieveCoordinates.emit(this.form.controls.name.value);
+    }
 }
diff --git a/client/src/app/instance/shared-search/components/datatable/datatable.component.html b/client/src/app/instance/shared-search/components/datatable/datatable.component.html
index 8a373adc94235177078fdbe92c629b1bb642024b..881f79065975906242993d75e2239f945bd4ff4c 100644
--- a/client/src/app/instance/shared-search/components/datatable/datatable.component.html
+++ b/client/src/app/instance/shared-search/components/datatable/datatable.component.html
@@ -45,6 +45,7 @@
                             <app-detail-renderer
                                 [value]="datum[attribute.label]"
                                 [datasetName]="dataset.name"
+                                [instanceSelected]="instanceSelected"
                                 [config]="getRendererConfig(attribute)">
                             </app-detail-renderer>
                         </div>
diff --git a/client/src/app/instance/shared-search/components/datatable/datatable.component.ts b/client/src/app/instance/shared-search/components/datatable/datatable.component.ts
index 56d115ee220a55a674fc8b3a6343871b3c6c8178..698cf0403bf1ea003acb77f3b9da144bb38132db 100644
--- a/client/src/app/instance/shared-search/components/datatable/datatable.component.ts
+++ b/client/src/app/instance/shared-search/components/datatable/datatable.component.ts
@@ -26,6 +26,7 @@ import { Pagination, PaginationOrder } from 'src/app/instance/store/models';
  */
 export class DatatableComponent implements OnInit {
     @Input() dataset: Dataset;
+    @Input() instanceSelected: string;
     @Input() attributeList: Attribute[];
     @Input() outputList: number[];
     @Input() dataLength: number;
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.html b/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.html
index 0b6336b1e1acb9cf348dc857b43bd2fedc62ddc2..37c9f3133e39d52496faf4585956836050038ccc 100644
--- a/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.html
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.html
@@ -1,4 +1,4 @@
-<a routerLink="/detail/{{ datasetName }}/{{ value }}" [target]="config.blank == true ? '_blank' : '_self'"
+<a routerLink="/instance/{{ instanceSelected }}/detail/{{ datasetName }}/{{ value }}" [target]="config.blank == true ? '_blank' : '_self'"
     [ngClass]="{'btn btn-outline-primary btn-sm' : config.display == 'text-button'}">
     {{ value }}
 </a>
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.ts b/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.ts
index 06411ef9ec5a95251f5cc49da809f0490eb95839..d6df4dfd8384e2931696c6807a5a0c97bfdeaefb 100644
--- a/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.ts
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.ts
@@ -23,5 +23,6 @@ import { DetailRendererConfig } from 'src/app/metamodel/models/renderers/detail-
 export class DetailRendererComponent {
     @Input() value: string | number;
     @Input() datasetName: string;
+    @Input() instanceSelected: string;
     @Input() config: DetailRendererConfig;
 }
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts
index 74fc0a7f9a9f67367faeae9ea88d2bd404a25a12..19bcc38572ede27f043ee814f2f330d76eabeb25 100644
--- a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts
@@ -11,6 +11,7 @@ import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
 
 import { DownloadRendererConfig } from 'src/app/metamodel/models/renderers/download-renderer-config.model';
 import { getHost } from 'src/app/shared/utils';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Component({
     selector: 'app-download-renderer',
@@ -26,13 +27,15 @@ export class DownloadRendererComponent {
     @Input() datasetName: string;
     @Input() config: DownloadRendererConfig;
 
+    constructor(private appConfig: AppConfigService) { }
+
     /**
      * Returns link href.
      *
      * @return string
      */
     getHref(): string {
-        return getHost() + '/download-file/' + this.datasetName + '/' + this.value;
+        return getHost(this.appConfig.apiUrl) + '/download-file/' + this.datasetName + '/' + this.value;
     }
 
     /**
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.ts b/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.ts
index 75521e7d2816346299a40bebc41cee587f5a0e40..677dfd73c7e0f8baf9cd8bd11e1a74015f49c42a 100644
--- a/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.ts
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.ts
@@ -13,7 +13,7 @@ import { BsModalService } from 'ngx-bootstrap/modal';
 import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service';
 
 import { ImageRendererConfig } from 'src/app/metamodel/models/renderers';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Component({
     selector: 'app-image-renderer',
@@ -28,11 +28,10 @@ export class ImageRendererComponent {
     @Input() value: string | number;
     @Input() datasetName: string;
     @Input() config: ImageRendererConfig;
-    private SERVICES_PATH: string = environment.servicesUrl;
 
     modalRef: BsModalRef;
 
-    constructor(private modalService: BsModalService) { }
+    constructor(private modalService: BsModalService, private appConfig: AppConfigService) { }
 
     openModal(template: TemplateRef<any>) {
         this.modalRef = this.modalService.show(template);
@@ -45,7 +44,7 @@ export class ImageRendererComponent {
      */
     getValue(): string {
         if (this.config.type === 'fits') {
-            return `${this.SERVICES_PATH}/fits-to-png/${this.datasetName}?filename=${this.value}`
+            return `${this.appConfig.servicesUrl}/fits-to-png/${this.datasetName}?filename=${this.value}`
                 + `&stretch=linear&pmin=0.25&pmax=99.75&axes=true`;
         } else {
             return this.value as string;
diff --git a/client/src/app/instance/shared-search/components/index.ts b/client/src/app/instance/shared-search/components/index.ts
index f3ee95d0784280eae6250d149041229fd9e5127f..1bd2ebbdd5b479facf234dd6ba38ab4ef88f589e 100644
--- a/client/src/app/instance/shared-search/components/index.ts
+++ b/client/src/app/instance/shared-search/components/index.ts
@@ -1,7 +1,7 @@
-// import { coneSearchComponents } from './cone-search';
+import { coneSearchComponents } from './cone-search';
 import { datatableComponents } from './datatable';
 
 export const sharedComponents = [
-    // coneSearchComponents,
+    coneSearchComponents,
     datatableComponents
 ];
diff --git a/client/src/app/instance/store/actions/cone-search.actions.ts b/client/src/app/instance/store/actions/cone-search.actions.ts
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..779b424153952b2ca4f4e0dba7157b5a97c55c0e 100644
--- a/client/src/app/instance/store/actions/cone-search.actions.ts
+++ b/client/src/app/instance/store/actions/cone-search.actions.ts
@@ -0,0 +1,18 @@
+/**
+ * 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, props } from '@ngrx/store';
+
+import { ConeSearch, Resolver } from '../models';
+
+export const addConeSearch = createAction('[ConeSearch] Add Cone Search', props<{ coneSearch: ConeSearch }>());
+export const deleteConeSearch = createAction('[ConeSearch] Delete Cone Search');
+export const retrieveCoordinates = createAction('[ConeSearch] Retrieve Coordinates', props<{ name: string }>());
+export const retrieveCoordinatesSuccess = createAction('[ConeSearch] Retrieve Coordinates Success', props<{ resolver: Resolver }>());
+export const retrieveCoordinatesFail = createAction('[ConeSearch] Retrieve Coordinates Fail');
diff --git a/client/src/app/instance/store/actions/detail.actions.ts b/client/src/app/instance/store/actions/detail.actions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c88210bc8e4e24fb0ee165d34c38c07a90792322
--- /dev/null
+++ b/client/src/app/instance/store/actions/detail.actions.ts
@@ -0,0 +1,17 @@
+/**
+ * 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, props } from '@ngrx/store';
+
+export const retrieveObject = createAction('[Detail] Retrieve Object');
+export const retrieveObjectSuccess = createAction('[Detail] Retrieve Object Success', props<{ object: any }>());
+export const retrieveObjectFail = createAction('[Detail] Retrieve Object Fail');
+export const retrieveSpectra = createAction('[Detail] Retrieve Spectra', props<{ filename: string }>());
+export const retrieveSpectraSuccess = createAction('[Detail] Retrieve Spectra Success', props<{ spectraCSV: string }>());
+export const retrieveSpectraFail = createAction('[Detail] Retrieve Spectra Fail');
diff --git a/client/src/app/instance/store/effects/cone-search.effects.ts b/client/src/app/instance/store/effects/cone-search.effects.ts
new file mode 100644
index 0000000000000000000000000000000000000000..84d2ab8fe05cc36bdba788ba155cd47ae0c7270d
--- /dev/null
+++ b/client/src/app/instance/store/effects/cone-search.effects.ts
@@ -0,0 +1,58 @@
+/**
+ * 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 } from '@ngrx/effects';
+import { of } from 'rxjs';
+import { map, tap, mergeMap, catchError } from 'rxjs/operators';
+import { ToastrService } from 'ngx-toastr';
+
+import * as coneSearchActions from '../actions/cone-search.actions';
+import { ConeSearchService } from '../services/cone-search.service';
+
+@Injectable()
+export class ConeSearchEffects {
+    retrieveCoordinates$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(coneSearchActions.retrieveCoordinates),
+            mergeMap((action) => this.coneSearchService.retrieveCoordinates(action.name)
+                .pipe(
+                    map(response => {
+                        const parser = new DOMParser();
+                        const xml = parser.parseFromString(response,'text/xml');
+                        if (xml.getElementsByTagName('Resolver').length === 0) {
+                            const name = xml.getElementsByTagName('name')[0].childNodes[0].nodeValue;
+                            return coneSearchActions.retrieveCoordinatesFail();
+                        }
+                        const name = xml.getElementsByTagName('name')[0].childNodes[0].nodeValue;
+                        const ra = +xml.getElementsByTagName('jradeg')[0].childNodes[0].nodeValue;
+                        const dec = +xml.getElementsByTagName('jdedeg')[0].childNodes[0].nodeValue;
+                        const resolver = { name, ra, dec };
+                        return coneSearchActions.retrieveCoordinatesSuccess({ resolver });
+                    }),
+                    catchError(() => of(coneSearchActions.retrieveCoordinatesFail()))
+                )
+            )
+        )
+    );
+
+    retrieveCoordinatesFail$ = createEffect(() => 
+        this.actions$.pipe(
+            ofType(coneSearchActions.retrieveCoordinatesFail),
+            tap(() => this.toastr.error('Failure to retrieve coordinates', 'The coordinates could not be retrieved'))
+        ), { dispatch: false}
+    );
+
+    constructor(
+        private actions$: Actions,
+        private coneSearchService: ConeSearchService,
+        private toastr: ToastrService
+    ) {}
+}
diff --git a/client/src/app/instance/store/effects/detail.effects.ts b/client/src/app/instance/store/effects/detail.effects.ts
new file mode 100644
index 0000000000000000000000000000000000000000..256412aea966b727115af6394a73795b181e22e7
--- /dev/null
+++ b/client/src/app/instance/store/effects/detail.effects.ts
@@ -0,0 +1,80 @@
+/**
+ * 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  } from '@ngrx/store';
+import { of } from 'rxjs';
+import { map, tap, mergeMap, catchError } from 'rxjs/operators';
+import { ToastrService } from 'ngx-toastr';
+
+import { DetailService } from '../services/detail.service';
+import * as detailActions from '../actions/detail.actions';
+import * as detailSelector from '../selectors/detail.selector';
+import * as attributeSelector from 'src/app/metamodel/selectors/attribute.selector';
+import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector';
+ 
+@Injectable()
+export class DetailEffects {
+    retrieveObject$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(detailActions.retrieveObject),
+            concatLatestFrom(() => [
+                this.store.select(datasetSelector.selectDatasetNameByRoute),
+                this.store.select(attributeSelector.selectAllAttributes),
+                this.store.select(detailSelector.selectIdByRoute)
+            ]),
+            mergeMap(([action, datasetName, attributeList, id]) => this.detailService.retrieveObject(
+                datasetName,
+                attributeList.find(attribute => attribute.order_by).id,
+                id,
+                attributeList.filter(attribute => attribute.detail).map(attribute => attribute.id)
+            ).pipe(
+                map(object => detailActions.retrieveObjectSuccess({ object: object[0] })),
+                catchError(() => of(detailActions.retrieveObjectFail()))
+            ))
+        )
+    );
+
+    retrieveObjectFail$ = createEffect(() => 
+        this.actions$.pipe(
+            ofType(detailActions.retrieveObjectFail),
+            tap(() => this.toastr.error('Loading Failed!', 'Unable to load the object'))
+        ), { dispatch: false}
+    );
+
+    retrieveSpectra$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(detailActions.retrieveSpectra),
+            concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)),
+            mergeMap(([action, datasetName]) => this.detailService.retrieveSpectra(
+                datasetName,
+                action.filename
+            ).pipe(
+                map(spectraCSV => detailActions.retrieveSpectraSuccess({ spectraCSV })),
+                catchError(() => of(detailActions.retrieveSpectraFail()))
+            ))
+        )
+    );
+
+    retrieveSpectraFail$ = createEffect(() => 
+        this.actions$.pipe(
+            ofType(detailActions.retrieveSpectraFail),
+            tap(() => this.toastr.error('Loading Failed!', 'Unable to load spectra'))
+        ), { dispatch: false}
+    );
+    
+    constructor(
+        private actions$: Actions,
+        private detailService: DetailService,
+        private store: Store<{ }>,
+        private toastr: ToastrService
+    ) {}
+}
diff --git a/client/src/app/instance/store/effects/index.ts b/client/src/app/instance/store/effects/index.ts
index 6c5a834023827b54a609d049daa42baa9b829be3..609cc1388dcbf3a2b47c1f739c0a8e22989ec94b 100644
--- a/client/src/app/instance/store/effects/index.ts
+++ b/client/src/app/instance/store/effects/index.ts
@@ -1,11 +1,15 @@
 import { MetamodelEffects } from './metamodel.effects';
-import { SampEffects } from "./samp.effects";
-import { SearchEffects } from "./search.effects";
+import { SampEffects } from './samp.effects';
+import { SearchEffects } from './search.effects';
+import { ConeSearchEffects } from './cone-search.effects';
+import { DetailEffects } from './detail.effects';
 import { DocumentationEffects } from "./documentation.effects";
 
 export const instanceEffects = [
     MetamodelEffects,
     SampEffects,
     SearchEffects,
+    ConeSearchEffects,
+    DetailEffects,
     DocumentationEffects
 ];
diff --git a/client/src/app/instance/store/effects/search.effects.ts b/client/src/app/instance/store/effects/search.effects.ts
index f3ffcd4d55f29538f39c453a2b763b179796226e..b92513c667482841cf169ebeef11fa595520adc9 100644
--- a/client/src/app/instance/store/effects/search.effects.ts
+++ b/client/src/app/instance/store/effects/search.effects.ts
@@ -15,7 +15,7 @@ import { of } from 'rxjs';
 import { map, tap, mergeMap, catchError } from 'rxjs/operators';
 import { ToastrService } from 'ngx-toastr';
 
-import { criterionToString, stringToCriterion } from '../models';
+import { ConeSearch, criterionToString, stringToCriterion } from '../models';
 import { SearchService } from '../services/search.service';
 import * as searchActions from '../actions/search.actions';
 import * as attributeActions from 'src/app/metamodel/actions/attribute.actions';
@@ -25,6 +25,8 @@ import * as outputFamilyActions from 'src/app/metamodel/actions/output-family.ac
 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';
+import * as coneSearchActions from '../actions/cone-search.actions';
+import * as coneSearchSelector from '../selectors/cone-search.selector';
 
 @Injectable()
 export class SearchEffects {
@@ -88,11 +90,19 @@ export class SearchEffects {
         this.actions$.pipe(
             ofType(searchActions.loadDefaultFormParameters),
             concatLatestFrom(() => [
+                this.store.select(searchSelector.selectPristine),
+                this.store.select(searchSelector.selectCurrentDataset),
                 this.store.select(attributeSelector.selectAllAttributes),
                 this.store.select(searchSelector.selectCriteriaListByRoute),
+                this.store.select(coneSearchSelector.selectConeSearchByRoute),
                 this.store.select(searchSelector.selectOutputListByRoute)
             ]),
-            mergeMap(([action, attributeList, criteriaList, outputList]) => {
+            mergeMap(([action, pristine, currentDataset, attributeList, criteriaList, coneSearch, outputList]) => {
+                if (!pristine || !currentDataset) {
+                    // Default form parameters already loaded or no dataset selected
+                    return of({ type: '[No Action] Load Default Form Parameters' });
+                }
+
                 // Update criteria list
                 let defaultCriteriaList = [];
                 if (criteriaList) {
@@ -109,6 +119,17 @@ export class SearchEffects {
                         .map(attribute => stringToCriterion(attribute));
                 }
 
+                // Update cone search
+                let defaultConeSearch: ConeSearch = null;
+                if (coneSearch) {
+                    const params = coneSearch.split(':');
+                    defaultConeSearch = {
+                        ra: +params[0],
+                        dec: +params[1],
+                        radius: +params[2]
+                    };
+                }
+
                 // Update output list
                 let defaultOutputList = [];
                 if (outputList) {
@@ -124,6 +145,7 @@ export class SearchEffects {
                 // Returns actions and mark the form as dirty
                 return [
                     searchActions.updateCriteriaList({ criteriaList: defaultCriteriaList }),
+                    coneSearchActions.addConeSearch({ coneSearch: defaultConeSearch }),
                     searchActions.updateOutputList({ outputList: defaultOutputList }),
                     searchActions.markAsDirty()
                 ];
@@ -136,13 +158,17 @@ export class SearchEffects {
             ofType(searchActions.retrieveDataLength),
             concatLatestFrom(() => [
                 this.store.select(datasetSelector.selectDatasetNameByRoute),
-                this.store.select(searchSelector.selectCriteriaList)
+                this.store.select(searchSelector.selectCriteriaList),
+                this.store.select(coneSearchSelector.selectConeSearch)
             ]),
-            mergeMap(([action, datasetName, criteriaList]) => {
+            mergeMap(([action, datasetName, criteriaList, coneSearch]) => {
                 let query = datasetName + '?a=count';
                 if (criteriaList.length > 0) {
                     query += '&c=' + criteriaList.map(criterion => criterionToString(criterion)).join(';');
                 }
+                if (coneSearch) {
+                    query += '&cs=' + coneSearch.ra + ':' + coneSearch.dec + ':' + coneSearch.radius;
+                }
 
                 return this.searchService.retrieveDataLength(query)
                     .pipe(
@@ -166,13 +192,18 @@ export class SearchEffects {
             concatLatestFrom(() => [
                 this.store.select(datasetSelector.selectDatasetNameByRoute),
                 this.store.select(searchSelector.selectCriteriaList),
+                this.store.select(coneSearchSelector.selectConeSearch),
                 this.store.select(searchSelector.selectOutputList)
             ]),
-            mergeMap(([action, datasetName, criteriaList, outputList]) => {
+            mergeMap(([action, datasetName, criteriaList, coneSearch, outputList]) => {
                 let query = datasetName + '?a=' + outputList.join(';');
                 if (criteriaList.length > 0) {
                     query += '&c=' + criteriaList.map(criterion => criterionToString(criterion)).join(';');
                 }
+                if (coneSearch) {
+                    query += '&cs=' + coneSearch.ra + ':' + coneSearch.dec + ':' + coneSearch.radius;
+                }
+
                 query += '&p=' + action.pagination.nbItems + ':' + action.pagination.page;
                 query += '&o=' + action.pagination.sortedCol + ':' + action.pagination.order;
 
diff --git a/client/src/app/instance/store/reducers/cone-search.reducer.ts b/client/src/app/instance/store/reducers/cone-search.reducer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6a4ee35ce53cc7f5271515cf2811850994e04353
--- /dev/null
+++ b/client/src/app/instance/store/reducers/cone-search.reducer.ts
@@ -0,0 +1,59 @@
+/**
+ * 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 { createReducer, on } from '@ngrx/store';
+
+import * as coneSearchActions from '../actions/cone-search.actions';
+import { ConeSearch, Resolver } from '../models';
+
+export interface State {
+    coneSearch: ConeSearch;
+    resolver: Resolver;
+    resolverIsLoading: boolean;
+    resolverIsLoaded: boolean;
+}
+
+export const initialState: State = {
+    coneSearch: null,
+    resolver: null,
+    resolverIsLoading: false,
+    resolverIsLoaded: false
+}
+
+export const coneSearchReducer = createReducer(
+    initialState,
+    on(coneSearchActions.addConeSearch, (state, { coneSearch }) => ({
+        ...state,
+        coneSearch
+    })),
+    on(coneSearchActions.deleteConeSearch, state => ({
+        ...state,
+        coneSearch: null
+    })),
+    on(coneSearchActions.retrieveCoordinates, state => ({
+        ...state,
+        resolverIsLoading: true,
+        resolverIsLoaded: false
+    })),
+    on(coneSearchActions.retrieveCoordinatesSuccess, (state, { resolver }) => ({
+        ...state,
+        resolver,
+        resolverIsLoading: false,
+        resolverIsLoaded: true
+    })),
+    on(coneSearchActions.retrieveCoordinatesFail, state => ({
+        ...state,
+        resolverIsLoading: false
+    }))
+);
+
+export const selectConeSearch = (state: State) => state.coneSearch;
+export const selectResolver = (state: State) => state.resolver;
+export const selectResolverIsLoading = (state: State) => state.resolverIsLoading;
+export const selectResolverIsLoaded = (state: State) => state.resolverIsLoaded;
diff --git a/client/src/app/instance/store/reducers/detail.reducer.ts b/client/src/app/instance/store/reducers/detail.reducer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..466fcfc1838c6e79d790cf87fafb944b96081189
--- /dev/null
+++ b/client/src/app/instance/store/reducers/detail.reducer.ts
@@ -0,0 +1,71 @@
+/**
+ * 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 { createReducer, on } from '@ngrx/store';
+
+import * as detailActions from '../actions/detail.actions';
+
+export interface State {
+    object: any;
+    objectIsLoading: boolean;
+    objectIsLoaded: boolean;
+    spectraCSV: string;
+    spectraIsLoading: boolean;
+    spectraIsLoaded: boolean;
+}
+
+export const initialState: State = {
+    object: null,
+    objectIsLoading: false,
+    objectIsLoaded: false,
+    spectraCSV: null,
+    spectraIsLoading: false,
+    spectraIsLoaded: false,
+};
+
+export const detailReducer = createReducer(
+    initialState,
+    on(detailActions.retrieveObject, state => ({
+        ...state,
+        objectIsLoading: true,
+        objectIsLoaded: false
+    })),
+    on(detailActions.retrieveObjectSuccess, (state, { object }) => ({
+        ...state,
+        objectIsLoading: false,
+        objectIsLoaded: true,
+        object
+    })),
+    on(detailActions.retrieveObjectFail, state => ({
+        ...state,
+        objectIsLoading: false
+    })),
+    on(detailActions.retrieveSpectra, state => ({
+        ...state,
+        spectraIsLoading: true,
+        spectraIsLoaded: false
+    })),
+    on(detailActions.retrieveSpectraSuccess, (state, { spectraCSV }) => ({
+        ...state,
+        spectraIsLoading: false,
+        spectraIsLoaded: true,
+        spectraCSV
+    })),
+    on(detailActions.retrieveSpectraFail, state => ({
+        ...state,
+        spectraIsLoading: false
+    }))
+);
+
+export const selectObject = (state: State) => state.object;
+export const selectObjectIsLoading = (state: State) => state.objectIsLoading;
+export const selectObjectIsLoaded = (state: State) => state.objectIsLoaded;
+export const selectSpectraCSV = (state: State) => state.spectraCSV;
+export const selectSpectraIsLoading = (state: State) => state.spectraIsLoading;
+export const selectSpectraIsLoaded = (state: State) => state.spectraIsLoaded;
diff --git a/client/src/app/instance/store/selectors/cone-search.selector.ts b/client/src/app/instance/store/selectors/cone-search.selector.ts
new file mode 100644
index 0000000000000000000000000000000000000000..695bba185eacc01db90b4dfface55f691ee3d3c7
--- /dev/null
+++ b/client/src/app/instance/store/selectors/cone-search.selector.ts
@@ -0,0 +1,43 @@
+/**
+ * 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 { createSelector } from '@ngrx/store';
+
+import * as reducer from '../../instance.reducer';
+import * as fromConeSearch from '../reducers/cone-search.reducer';
+
+export const selectConeSearchState = createSelector(
+    reducer.getInstanceState,
+    (state: reducer.State) => state.coneSearch
+);
+
+export const selectConeSearch = createSelector(
+    selectConeSearchState,
+    fromConeSearch.selectConeSearch
+);
+
+export const selectResolver = createSelector(
+    selectConeSearchState,
+    fromConeSearch.selectResolver
+);
+
+export const selectResolverIsLoading = createSelector(
+    selectConeSearchState,
+    fromConeSearch.selectResolverIsLoading
+);
+
+export const selectResolverIsLoaded = createSelector(
+    selectConeSearchState,
+    fromConeSearch.selectResolverIsLoaded
+);
+
+export const selectConeSearchByRoute = createSelector(
+    reducer.selectRouterState,
+    router => router.state.queryParams.cs as string
+);
diff --git a/client/src/app/instance/store/selectors/detail.selector.ts b/client/src/app/instance/store/selectors/detail.selector.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2fe62d5722257342c255c5b4680e6883ecad19f5
--- /dev/null
+++ b/client/src/app/instance/store/selectors/detail.selector.ts
@@ -0,0 +1,53 @@
+/**
+ * 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 { createSelector } from '@ngrx/store';
+
+import * as reducer from '../../instance.reducer';
+import * as fromDetail from '../reducers/detail.reducer';
+
+export const selectDetailState = createSelector(
+    reducer.getInstanceState,
+    (state: reducer.State) => state.detail
+);
+
+export const selectObject = createSelector(
+    selectDetailState,
+    fromDetail.selectObject
+);
+
+export const selectObjectIsLoading = createSelector(
+    selectDetailState,
+    fromDetail.selectObjectIsLoading
+);
+
+export const selectObjectIsLoaded = createSelector(
+    selectDetailState,
+    fromDetail.selectObjectIsLoaded
+);
+
+export const selectSpectraCSV = createSelector(
+    selectDetailState,
+    fromDetail.selectSpectraCSV
+);
+
+export const selectSpectraIsLoading = createSelector(
+    selectDetailState,
+    fromDetail.selectSpectraIsLoading
+);
+
+export const selectSpectraIsLoaded = createSelector(
+    selectDetailState,
+    fromDetail.selectSpectraIsLoaded
+);
+
+export const selectIdByRoute = createSelector(
+    reducer.selectRouterState,
+    router => router.state.params.id
+);
diff --git a/client/src/app/instance/store/selectors/search.selector.ts b/client/src/app/instance/store/selectors/search.selector.ts
index ee70032ca896242b28fe17f59bd61982c7159f74..44ebe388b9cde19e8bbfe3eff35a5bb9e7b68ecd 100644
--- a/client/src/app/instance/store/selectors/search.selector.ts
+++ b/client/src/app/instance/store/selectors/search.selector.ts
@@ -9,103 +9,106 @@
 
 import { createSelector } from '@ngrx/store';
 
-import { Criterion, SearchQueryParams, criterionToString } from '../models';
+import { Criterion, SearchQueryParams, criterionToString, ConeSearch } from '../models';
 import * as reducer from '../../instance.reducer';
 import * as fromSearch from '../reducers/search.reducer';
+import * as coneSearchSelector from './cone-search.selector';
 
-export const selectInstanceState = createSelector(
+export const selectSearchState = createSelector(
     reducer.getInstanceState,
     (state: reducer.State) => state.search
 );
 
 export const selectPristine = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectPristine
 );
 
 export const selectCurrentDataset = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectCurrentDataset
 );
 
 export const selectCurrentStep = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectCurrentStep
 );
 
 export const selectCriteriaStepChecked = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectCriteriaStepChecked
 );
 
 export const selectOutputStepChecked = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectOutputStepChecked
 );
 
 export const selectResultStepChecked = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectResultStepChecked
 );
 
 export const selectIsConeSearchAdded = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectIsConeSearchAdded
 );
 
 export const selectCriteriaList = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectCriteriaList
 );
 
 export const selectOutputList = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectOutputList
 );
 
 export const selectDataLengthIsLoading = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectDataLengthIsLoading
 );
 
 export const selectDataLengthIsLoaded = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectDataLengthIsLoaded
 );
 
 export const selectDataLength = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectDataLength
 );
 
 export const selectDataIsLoading = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectDataIsLoading
 );
 
 export const selectDataIsLoaded = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectDataIsLoaded
 );
 
 export const selectData = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectData
 );
 
 export const selectSelectedData = createSelector(
-    selectInstanceState,
+    selectSearchState,
     fromSearch.selectSelectedData
 );
 
 export const selectQueryParams = createSelector(
     selectCriteriaList,
+    coneSearchSelector.selectConeSearch,
     selectOutputList,
     selectCriteriaStepChecked,
     selectOutputStepChecked,
     selectResultStepChecked,
     (
         criteriaList: Criterion[],
+        coneSearch: ConeSearch,
         outputList: number[],
         criteriaStepChecked: boolean,
         outputStepChecked: boolean,
@@ -118,6 +121,12 @@ export const selectQueryParams = createSelector(
             s: step,
             a: outputList.join(';')
         };
+        if (coneSearch) {
+            queryParams = {
+                ...queryParams,
+                cs: coneSearch.ra + ':' + coneSearch.dec + ':' + coneSearch.radius
+            };
+        }
         if (criteriaList.length > 0) {
             queryParams = {
                 ...queryParams,
diff --git a/client/src/app/instance/store/services/cone-search.service.ts b/client/src/app/instance/store/services/cone-search.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..37c3420e5f1af5b426de2cf6bc6c8885e152f8bb
--- /dev/null
+++ b/client/src/app/instance/store/services/cone-search.service.ts
@@ -0,0 +1,33 @@
+/**
+ * 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 { HttpClient } from '@angular/common/http';
+import { Observable } from 'rxjs';
+
+@Injectable()
+/**
+ * @class
+ * @classdesc Cone search service.
+ */
+export class ConeSearchService {
+    constructor(private http: HttpClient) { }
+
+    /**
+     * Retrieves coordinates of an object name.
+     *
+     * @param  {string} name - The name of the object.
+     *
+     * @return Observable<any>
+     */
+    retrieveCoordinates(name: string): Observable<any> {
+        const url = 'https://cdsweb.u-strasbg.fr/cgi-bin/nph-sesame/-ox/NSV?' + name;
+        return this.http.get(url, { responseType: 'text' });
+    }
+}
diff --git a/client/src/app/instance/store/services/detail.service.ts b/client/src/app/instance/store/services/detail.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3b946c5b17a2e9c649be86c4fa267ebe32f4907d
--- /dev/null
+++ b/client/src/app/instance/store/services/detail.service.ts
@@ -0,0 +1,50 @@
+/**
+ * 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 { HttpClient } from '@angular/common/http';
+
+import { Observable } from 'rxjs';
+
+import { AppConfigService } from 'src/app/app-config.service';
+
+@Injectable()
+/**
+ * @class
+ * @classdesc Detail service.
+ */
+export class DetailService {
+    constructor(private http: HttpClient, private config: AppConfigService) { }
+
+    /**
+     * Retrieves object details for the given parameters.
+     *
+     * @param  {string} dname - The dataset name.
+     * @param  {number} criterionId - The criterion ID.
+     * @param  {string} objectSelected - The selected object ID.
+     * @param  {number[]} outputList - The output list.
+     *
+     * @return Observable<any[]>
+     */
+    retrieveObject(dname: string, criterionId: number, objectSelected: string, outputList: number[]): Observable<any[]> {
+        const query = dname + '?c=' + criterionId + '::eq::' + objectSelected + '&a=' + outputList.join(';');
+        return this.http.get<any[]>(this.config.apiUrl + '/search/' + query);
+    }
+
+    /**
+     * Retrieves object details for the given parameters.
+     *
+     * @param  {string} spectraFile - The spectra file name.
+     *
+     * @return Observable<string>
+     */
+    retrieveSpectra(dname: string, spectraFile: string): Observable<string> {
+        return this.http.get(this.config.servicesUrl + '/spectra-to-csv/' + dname + '?filename=' + spectraFile, { responseType: 'text' });
+    }
+}
diff --git a/client/src/app/instance/store/services/index.ts b/client/src/app/instance/store/services/index.ts
index 136f9cb320ff0395ea365076e1f7753d31f9f680..cabc079690c1bfea8a44fddb9baa7123796937b2 100644
--- a/client/src/app/instance/store/services/index.ts
+++ b/client/src/app/instance/store/services/index.ts
@@ -1,7 +1,11 @@
 import { SearchService } from './search.service';
 import { SampService } from './samp.service';
+import { ConeSearchService } from './cone-search.service';
+import { DetailService } from './detail.service';
 
 export const instanceServices = [
     SearchService,
-    SampService
+    SampService,
+    ConeSearchService,
+    DetailService
 ];
diff --git a/client/src/app/instance/store/services/samp.service.ts b/client/src/app/instance/store/services/samp.service.ts
index c4a8a9dbb612ac46a8690fe0d70826875aade414..1429c9bf7394825137519be7bf9830021822d274 100644
--- a/client/src/app/instance/store/services/samp.service.ts
+++ b/client/src/app/instance/store/services/samp.service.ts
@@ -11,7 +11,7 @@ import { Injectable } from '@angular/core';
 
 import { Observable } from 'rxjs';
 
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 declare var samp: any;
 
@@ -23,8 +23,8 @@ declare var samp: any;
 export class SampService {
     private connector = null;
     
-    constructor() {
-        const baseUrl = window.location.protocol + "//" + window.location.host + environment.baseHref;
+    constructor(private config: AppConfigService) {
+        const baseUrl = window.location.protocol + "//" + window.location.host + this.config.baseHref;
         const meta = {
             "samp.name": "ANIS",
             "samp.description.text": "AstroNomical Information System",
diff --git a/client/src/app/instance/store/services/search.service.ts b/client/src/app/instance/store/services/search.service.ts
index 7f5321317525144fe8d1e7da4670f892d5ac6d7c..171814321dc1e71d546be92e53c614ccd4d36410 100644
--- a/client/src/app/instance/store/services/search.service.ts
+++ b/client/src/app/instance/store/services/search.service.ts
@@ -12,7 +12,7 @@ import { HttpClient } from '@angular/common/http';
 
 import { Observable } from 'rxjs';
 
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 /**
@@ -20,9 +20,7 @@ import { environment } from 'src/environments/environment';
  * @classdesc Search service.
  */
 export class SearchService {
-    API_PATH: string = environment.apiUrl;
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     /**
      * Retrieves results for the given parameters.
@@ -32,7 +30,7 @@ export class SearchService {
      * @return Observable<any[]>
      */
     retrieveData(query: string): Observable<any[]> {
-        return this.http.get<any[]>(this.API_PATH + '/search/' + query);
+        return this.http.get<any[]>(this.config.apiUrl + '/search/' + query);
     }
 
     /**
@@ -43,6 +41,6 @@ export class SearchService {
      * @return Observable<{ nb: number }[]>
      */
     retrieveDataLength(query: string): Observable<{ nb: number }[]> {
-        return this.http.get<{ nb: number }[]>(this.API_PATH + '/search/' + query);
+        return this.http.get<{ nb: number }[]>(this.config.apiUrl + '/search/' + query);
     }
 }
diff --git a/client/src/app/metamodel/services/attribute-distinct.service.ts b/client/src/app/metamodel/services/attribute-distinct.service.ts
index bc89ba7c5f2127f4eda6d7fb5b24e91b66eb42f7..e5466159309a507362dfe260e8f33073516a4fd5 100644
--- a/client/src/app/metamodel/services/attribute-distinct.service.ts
+++ b/client/src/app/metamodel/services/attribute-distinct.service.ts
@@ -13,15 +13,13 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { Attribute } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class AttributeDistinctService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveAttributeDistinctList(datasetName: string, attribute: Attribute): Observable<string[]> {
-        return this.http.get<string[]>(this.API_PATH + 'dataset/' + datasetName + '/attribute/' + attribute.id + '/distinct');
+        return this.http.get<string[]>(this.config.apiUrl + '/dataset/' + datasetName + '/attribute/' + attribute.id + '/distinct');
     }
 }
diff --git a/client/src/app/metamodel/services/attribute.service.ts b/client/src/app/metamodel/services/attribute.service.ts
index 08c84d6cfbc6ac70cfa3c755d00c82d4bce46860..9c2583c71f1c1af80bda0ad53daa8c9402ff11fc 100644
--- a/client/src/app/metamodel/services/attribute.service.ts
+++ b/client/src/app/metamodel/services/attribute.service.ts
@@ -13,27 +13,25 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { Attribute } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class AttributeService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveAttributeList(datasetName: string): Observable<Attribute[]> {
-        return this.http.get<Attribute[]>(this.API_PATH + 'dataset/' + datasetName + '/attribute');
+        return this.http.get<Attribute[]>(this.config.apiUrl + '/dataset/' + datasetName + '/attribute');
     }
 
     addAttribute(datasetName: string, attribute: Attribute): Observable<Attribute> {
-        return this.http.post<Attribute>(this.API_PATH + 'dataset/' + datasetName + '/attribute', attribute);
+        return this.http.post<Attribute>(this.config.apiUrl + '/dataset/' + datasetName + '/attribute', attribute);
     }
 
     editAttribute(datasetName: string, attribute: Attribute): Observable<Attribute> {
-        return this.http.put<Attribute>(this.API_PATH + 'dataset/' + datasetName + '/attribute/' + attribute.id, attribute);
+        return this.http.put<Attribute>(this.config.apiUrl + '/dataset/' + datasetName + '/attribute/' + attribute.id, attribute);
     }
 
     deleteAttribute(datasetName: string, attribute: Attribute) {
-        return this.http.delete(this.API_PATH + 'dataset/' + datasetName + '/attribute/' + attribute.id); 
+        return this.http.delete(this.config.apiUrl + '/dataset/' + datasetName + '/attribute/' + attribute.id); 
     }
 }
diff --git a/client/src/app/metamodel/services/column.service.ts b/client/src/app/metamodel/services/column.service.ts
index 881006be1a815b3960e5be1d012e1dce0a25d97d..0da2af5345283d3374916e1fc40b4bc4cd9ef2cd 100644
--- a/client/src/app/metamodel/services/column.service.ts
+++ b/client/src/app/metamodel/services/column.service.ts
@@ -13,15 +13,13 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { Column } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class ColumnService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveColumns(datasetName: string): Observable<Column[]> {
-        return this.http.get<Column[]>(this.API_PATH + 'dataset/' + datasetName + '/column');
+        return this.http.get<Column[]>(this.config.apiUrl + '/dataset/' + datasetName + '/column');
     }
 }
diff --git a/client/src/app/metamodel/services/criteria-family.service.ts b/client/src/app/metamodel/services/criteria-family.service.ts
index 32e2ecfabedacdf8282e5e97626e35d25ec6d5a3..09d6dd21a477f150f8de5f5fe0a6796e0860966f 100644
--- a/client/src/app/metamodel/services/criteria-family.service.ts
+++ b/client/src/app/metamodel/services/criteria-family.service.ts
@@ -13,27 +13,25 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { CriteriaFamily } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class CriteriaFamilyService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveCriteriaFamilyList(datasetName: string): Observable<CriteriaFamily[]> {
-        return this.http.get<CriteriaFamily[]>(this.API_PATH + 'dataset/' + datasetName + '/criteria-family');
+        return this.http.get<CriteriaFamily[]>(this.config.apiUrl + '/dataset/' + datasetName + '/criteria-family');
     }
 
     addCriteriaFamily(datasetName: string, newCriteriaFamily: CriteriaFamily): Observable<CriteriaFamily> {
-        return this.http.post<CriteriaFamily>(this.API_PATH + 'dataset/' + datasetName + '/criteria-family', newCriteriaFamily);
+        return this.http.post<CriteriaFamily>(this.config.apiUrl + '/dataset/' + datasetName + '/criteria-family', newCriteriaFamily);
     }
 
     editCriteriaFamily(criteriaFamily: CriteriaFamily): Observable<CriteriaFamily> {
-        return this.http.put<CriteriaFamily>(this.API_PATH + 'criteria-family/' + criteriaFamily.id, criteriaFamily);
+        return this.http.put<CriteriaFamily>(this.config.apiUrl + '/criteria-family/' + criteriaFamily.id, criteriaFamily);
     }
 
     deleteCriteriaFamily(criteriaFamilyId: number) {
-        return this.http.delete(this.API_PATH + 'criteria-family/' + criteriaFamilyId);
+        return this.http.delete(this.config.apiUrl + '/criteria-family/' + criteriaFamilyId);
     }
 }
diff --git a/client/src/app/metamodel/services/database.service.ts b/client/src/app/metamodel/services/database.service.ts
index 567254fd3a852f25994ffc31b4264168bd092164..c569d351670179e73ccf3067dafa12bfdd6628e3 100644
--- a/client/src/app/metamodel/services/database.service.ts
+++ b/client/src/app/metamodel/services/database.service.ts
@@ -13,27 +13,25 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { Database } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class DatabaseService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveDatabaseList(): Observable<Database[]> {
-        return this.http.get<Database[]>(this.API_PATH + 'database');
+        return this.http.get<Database[]>(this.config.apiUrl + '/database');
     }
 
     addDatabase(newDatabase: Database): Observable<Database> {
-        return this.http.post<Database>(this.API_PATH + 'database', newDatabase);
+        return this.http.post<Database>(this.config.apiUrl + '/database', newDatabase);
     }
 
     editDatabase(database: Database): Observable<Database> {
-        return this.http.put<Database>(this.API_PATH + 'database/' + database.id, database);
+        return this.http.put<Database>(this.config.apiUrl + '/database/' + database.id, database);
     }
 
     deleteDatabase(databaseId: number) {
-        return this.http.delete(this.API_PATH + 'database/' + databaseId);
+        return this.http.delete(this.config.apiUrl + '/database/' + databaseId);
     }
 }
diff --git a/client/src/app/metamodel/services/dataset-family.service.ts b/client/src/app/metamodel/services/dataset-family.service.ts
index 490a14af0b92afe8c121d4d54938b489826e51c6..96fc34460b1c5520a754f5f1afb9edafcde8d821 100644
--- a/client/src/app/metamodel/services/dataset-family.service.ts
+++ b/client/src/app/metamodel/services/dataset-family.service.ts
@@ -13,27 +13,25 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { DatasetFamily } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class DatasetFamilyService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveDatasetFamilyList(instanceName: string): Observable<DatasetFamily[]> {
-        return this.http.get<DatasetFamily[]>(this.API_PATH + 'instance/' + instanceName + '/dataset-family');
+        return this.http.get<DatasetFamily[]>(this.config.apiUrl + '/instance/' + instanceName + '/dataset-family');
     }
 
     addDatasetFamily(instanceName: string, newDatasetFamily: DatasetFamily): Observable<DatasetFamily> {
-        return this.http.post<DatasetFamily>(this.API_PATH + 'instance/' + instanceName + '/dataset-family', newDatasetFamily);
+        return this.http.post<DatasetFamily>(this.config.apiUrl + '/instance/' + instanceName + '/dataset-family', newDatasetFamily);
     }
 
     editDatasetFamily(datasetFamily: DatasetFamily): Observable<DatasetFamily> {
-        return this.http.put<DatasetFamily>(this.API_PATH + 'dataset-family/' + datasetFamily.id, datasetFamily);
+        return this.http.put<DatasetFamily>(this.config.apiUrl + '/dataset-family/' + datasetFamily.id, datasetFamily);
     }
 
     deleteDatasetFamily(datasetFamilyId: number) {
-        return this.http.delete(this.API_PATH + 'dataset-family/' + datasetFamilyId);
+        return this.http.delete(this.config.apiUrl + '/dataset-family/' + datasetFamilyId);
     }
 }
diff --git a/client/src/app/metamodel/services/dataset.service.ts b/client/src/app/metamodel/services/dataset.service.ts
index 4b7c9b8369c11e0f9d2682ea36bd2f16f6a11d05..e8109448278f7e10f9e0f486eb25513c988c3265 100644
--- a/client/src/app/metamodel/services/dataset.service.ts
+++ b/client/src/app/metamodel/services/dataset.service.ts
@@ -13,27 +13,25 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { Dataset } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class DatasetService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveDatasetList(instanceName: string): Observable<Dataset[]> {
-        return this.http.get<Dataset[]>(this.API_PATH + 'instance/' + instanceName + '/dataset');
+        return this.http.get<Dataset[]>(this.config.apiUrl + '/instance/' + instanceName + '/dataset');
     }
 
     addDataset(newDataset: Dataset): Observable<Dataset> {
-        return this.http.post<Dataset>(this.API_PATH + 'dataset-family/' + newDataset.id_dataset_family + '/dataset', newDataset);
+        return this.http.post<Dataset>(this.config.apiUrl + '/dataset-family/' + newDataset.id_dataset_family + '/dataset', newDataset);
     }
 
     editDataset(dataset: Dataset): Observable<Dataset> {
-        return this.http.put<Dataset>(this.API_PATH + 'dataset/' + dataset.name, dataset);
+        return this.http.put<Dataset>(this.config.apiUrl + '/dataset/' + dataset.name, dataset);
     }
 
     deleteDataset(datasetName: string) {
-        return this.http.delete(this.API_PATH + 'dataset/' + datasetName);
+        return this.http.delete(this.config.apiUrl + '/dataset/' + datasetName);
     }
 }
diff --git a/client/src/app/metamodel/services/group.service.ts b/client/src/app/metamodel/services/group.service.ts
index c4cbdde647f2b446576a48834e6de42d2913995b..b83489bebbbc04d63fcbfb237ef359266bd27348 100644
--- a/client/src/app/metamodel/services/group.service.ts
+++ b/client/src/app/metamodel/services/group.service.ts
@@ -13,27 +13,25 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { Group } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class GroupService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveGroupList(instanceName: string): Observable<Group[]> {
-        return this.http.get<Group[]>(this.API_PATH + 'instance/' + instanceName +  '/group');
+        return this.http.get<Group[]>(this.config.apiUrl + '/instance/' + instanceName +  '/group');
     }
 
     addGroup(instanceName: string, newGroup: Group): Observable<Group> {
-        return this.http.post<Group>(this.API_PATH + 'instance/' + instanceName + '/group', newGroup);
+        return this.http.post<Group>(this.config.apiUrl + '/instance/' + instanceName + '/group', newGroup);
     }
 
     editGroup(group: Group): Observable<Group> {
-        return this.http.put<Group>(this.API_PATH + 'group/' + group.id, group);
+        return this.http.put<Group>(this.config.apiUrl + '/group/' + group.id, group);
     }
 
     deleteGroup(groupId: number) {
-        return this.http.delete(this.API_PATH + 'group/' + groupId);
+        return this.http.delete(this.config.apiUrl + '/group/' + groupId);
     }
 }
diff --git a/client/src/app/metamodel/services/instance.service.ts b/client/src/app/metamodel/services/instance.service.ts
index a5f4ab0e57f7dbdd4db7cebf56ff7ae8179e472a..d90f7f157acc9051687bd93469a49c46f9aa6e7f 100644
--- a/client/src/app/metamodel/services/instance.service.ts
+++ b/client/src/app/metamodel/services/instance.service.ts
@@ -13,27 +13,25 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { Instance } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class InstanceService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveInstanceList(): Observable<Instance[]> {
-        return this.http.get<Instance[]>(this.API_PATH + 'instance');
+        return this.http.get<Instance[]>(this.config.apiUrl + '/instance');
     }
 
     addInstance(newInstance: Instance): Observable<Instance> {
-        return this.http.post<Instance>(this.API_PATH + 'instance', newInstance);
+        return this.http.post<Instance>(this.config.apiUrl + '/instance', newInstance);
     }
 
     editInstance(instance: Instance): Observable<Instance> {
-        return this.http.put<Instance>(this.API_PATH + 'instance/' + instance.name, instance);
+        return this.http.put<Instance>(this.config.apiUrl + '/instance/' + instance.name, instance);
     }
 
     deleteInstance(instanceName: string) {
-        return this.http.delete(this.API_PATH + 'instance/' + instanceName);
+        return this.http.delete(this.config.apiUrl + '/instance/' + instanceName);
     }
 }
diff --git a/client/src/app/metamodel/services/output-category.service.ts b/client/src/app/metamodel/services/output-category.service.ts
index 2d35525d6071b2c049dba7025a8b42f943e0005f..5cc3e49dbd165c348df8dc60b7389b4592ff4e45 100644
--- a/client/src/app/metamodel/services/output-category.service.ts
+++ b/client/src/app/metamodel/services/output-category.service.ts
@@ -13,27 +13,25 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { OutputCategory } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class OutputCategoryService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveOutputCategoryList(datasetName: string): Observable<OutputCategory[]> {
-        return this.http.get<OutputCategory[]>(this.API_PATH + 'dataset/' + datasetName + '/output-category');
+        return this.http.get<OutputCategory[]>(this.config.apiUrl + '/dataset/' + datasetName + '/output-category');
     }
 
     addOutputCategory(newOutputCategory: OutputCategory): Observable<OutputCategory> {
-        return this.http.post<OutputCategory>(this.API_PATH + 'output-family/' + newOutputCategory.id_output_family + '/output-category', newOutputCategory);
+        return this.http.post<OutputCategory>(this.config.apiUrl + '/output-family/' + newOutputCategory.id_output_family + '/output-category', newOutputCategory);
     }
 
     editOutputCategory(outputCategory: OutputCategory): Observable<OutputCategory> {
-        return this.http.put<OutputCategory>(this.API_PATH + 'output-category/' + outputCategory.id, outputCategory);
+        return this.http.put<OutputCategory>(this.config.apiUrl + '/output-category/' + outputCategory.id, outputCategory);
     }
 
     deleteOutputCategory(outputCategoryId: number) {
-        return this.http.delete(this.API_PATH + 'output-category/' + outputCategoryId);
+        return this.http.delete(this.config.apiUrl + '/output-category/' + outputCategoryId);
     }
 }
diff --git a/client/src/app/metamodel/services/output-family.service.ts b/client/src/app/metamodel/services/output-family.service.ts
index 61494ed2cbcf8b2da2959c40290073c97cd55763..825e191902bac95960727424fdbe483c9f9d1a5a 100644
--- a/client/src/app/metamodel/services/output-family.service.ts
+++ b/client/src/app/metamodel/services/output-family.service.ts
@@ -13,27 +13,25 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { OutputFamily } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class OutputFamilyService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveOutputFamilyList(datasetName: string): Observable<OutputFamily[]> {
-        return this.http.get<OutputFamily[]>(this.API_PATH + 'dataset/' + datasetName + '/output-family');
+        return this.http.get<OutputFamily[]>(this.config.apiUrl + '/dataset/' + datasetName + '/output-family');
     }
 
     addOutputFamily(datasetName: string, newOutputFamily: OutputFamily): Observable<OutputFamily> {
-        return this.http.post<OutputFamily>(this.API_PATH + 'dataset/' + datasetName + '/output-family', newOutputFamily);
+        return this.http.post<OutputFamily>(this.config.apiUrl + '/dataset/' + datasetName + '/output-family', newOutputFamily);
     }
 
     editOutputFamily(criteriaFamily: OutputFamily): Observable<OutputFamily> {
-        return this.http.put<OutputFamily>(this.API_PATH + 'output-family/' + criteriaFamily.id, criteriaFamily);
+        return this.http.put<OutputFamily>(this.config.apiUrl + '/output-family/' + criteriaFamily.id, criteriaFamily);
     }
 
     deleteOutputFamily(outputFamilyId: number) {
-        return this.http.delete(this.API_PATH + 'output-family/' + outputFamilyId);
+        return this.http.delete(this.config.apiUrl + '/output-family/' + outputFamilyId);
     }
 }
diff --git a/client/src/app/metamodel/services/root-directory.service.ts b/client/src/app/metamodel/services/root-directory.service.ts
index c34f2c41a9f92d24917e928aca14e97b4f135610..d173f00d10b99e3872109fac4823c225a2a5a674 100644
--- a/client/src/app/metamodel/services/root-directory.service.ts
+++ b/client/src/app/metamodel/services/root-directory.service.ts
@@ -13,15 +13,13 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { FileInfo } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class RootDirectoryService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveRootDirectory(path: string): Observable<FileInfo[]> {
-        return this.http.get<FileInfo[]>(this.API_PATH + 'file-explorer/' + path);
+        return this.http.get<FileInfo[]>(this.config.apiUrl + '/file-explorer/' + path);
     }
 }
diff --git a/client/src/app/metamodel/services/select-option.service.ts b/client/src/app/metamodel/services/select-option.service.ts
index 07012cfab5b1126b321c6bc5970229b24af65e7c..e18a0dd35e8b55fce4006525e6705909ec208881 100644
--- a/client/src/app/metamodel/services/select-option.service.ts
+++ b/client/src/app/metamodel/services/select-option.service.ts
@@ -13,27 +13,25 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { SelectOption } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class SelectOptionService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveSelectOptionList(): Observable<SelectOption[]> {
-        return this.http.get<SelectOption[]>(this.API_PATH + 'option');
+        return this.http.get<SelectOption[]>(this.config.apiUrl + '/option');
     }
 
     addSelectOption(settingsSelectOption: SelectOption): Observable<SelectOption> {
-        return this.http.post<SelectOption>(this.API_PATH + 'option', settingsSelectOption);
+        return this.http.post<SelectOption>(this.config.apiUrl + '/option', settingsSelectOption);
     }
 
     editSelectOption(settingsSelectOption: SelectOption): Observable<SelectOption> {
-        return this.http.put<SelectOption>(this.API_PATH + 'option/' + settingsSelectOption.id, settingsSelectOption);
+        return this.http.put<SelectOption>(this.config.apiUrl + '/option/' + settingsSelectOption.id, settingsSelectOption);
     }
 
     deleteSelectOption(id: number) {
-        return this.http.delete(this.API_PATH + 'option/' + id);
+        return this.http.delete(this.config.apiUrl + '/option/' + id);
     }
 }
diff --git a/client/src/app/metamodel/services/select.service.ts b/client/src/app/metamodel/services/select.service.ts
index e88b7289c73060638d29462bc0fa79c5ea3d18fe..b146a874930054099765f9939310be7be1ab7c6b 100644
--- a/client/src/app/metamodel/services/select.service.ts
+++ b/client/src/app/metamodel/services/select.service.ts
@@ -13,27 +13,25 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { Select } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class SelectService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveSelectList(): Observable<Select[]> {
-        return this.http.get<Select[]>(this.API_PATH + 'select');
+        return this.http.get<Select[]>(this.config.apiUrl + '/select');
     }
 
     addSelect(select: Select): Observable<Select> {
-        return this.http.post<Select>(this.API_PATH + 'select', select);
+        return this.http.post<Select>(this.config.apiUrl + '/select', select);
     }
 
     editSelect(select: Select): Observable<Select> {
-        return this.http.put<Select>(this.API_PATH + 'select/' + select.name, select);
+        return this.http.put<Select>(this.config.apiUrl + '/select/' + select.name, select);
     }
 
     deleteSelect(name: string) {
-        return this.http.delete(this.API_PATH + 'select/' + name);
+        return this.http.delete(this.config.apiUrl + '/select/' + name);
     }
 }
diff --git a/client/src/app/metamodel/services/survey.service.ts b/client/src/app/metamodel/services/survey.service.ts
index 6f482874f18e4d0eb4a8bdd64d89f9d93359604a..faf0c1ea4d6eff2c1d72c60f2fcf551b81f843bd 100644
--- a/client/src/app/metamodel/services/survey.service.ts
+++ b/client/src/app/metamodel/services/survey.service.ts
@@ -13,27 +13,25 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 
 import { Survey } from '../models';
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class SurveyService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveSurveyList(): Observable<Survey[]> {
-        return this.http.get<Survey[]>(this.API_PATH + 'survey');
+        return this.http.get<Survey[]>(this.config.apiUrl + '/survey');
     }
 
     addSurvey(newSurvey: Survey): Observable<Survey> {
-        return this.http.post<Survey>(this.API_PATH + 'survey', newSurvey);
+        return this.http.post<Survey>(this.config.apiUrl + '/survey', newSurvey);
     }
 
     editSurvey(survey: Survey): Observable<Survey> {
-        return this.http.put<Survey>(this.API_PATH + 'survey/' + survey.name, survey);
+        return this.http.put<Survey>(this.config.apiUrl + '/survey/' + survey.name, survey);
     }
 
     deleteSurvey(surveyName: string) {
-        return this.http.delete(this.API_PATH + 'survey/' + surveyName);
+        return this.http.delete(this.config.apiUrl + '/survey/' + surveyName);
     }
 }
diff --git a/client/src/app/metamodel/services/table.service.ts b/client/src/app/metamodel/services/table.service.ts
index 96625dfd1e4221867c42c946a1ee262397c7aacf..3a3722b7f515825255cc08de75447101e4f65e5a 100644
--- a/client/src/app/metamodel/services/table.service.ts
+++ b/client/src/app/metamodel/services/table.service.ts
@@ -12,15 +12,13 @@ import { HttpClient } from '@angular/common/http';
 
 import { Observable } from 'rxjs';
 
-import { environment } from 'src/environments/environment';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Injectable()
 export class TableService {
-    private API_PATH: string = environment.apiUrl + '/';
-
-    constructor(private http: HttpClient) { }
+    constructor(private http: HttpClient, private config: AppConfigService) { }
 
     retrieveTableList(idDatabase: number): Observable<string[]> {
-        return this.http.get<string[]>(this.API_PATH + 'database/' + idDatabase + '/table');
+        return this.http.get<string[]>(this.config.apiUrl + '/database/' + idDatabase + '/table');
     }
 }
diff --git a/client/src/app/portal/containers/portal-home.component.html b/client/src/app/portal/containers/portal-home.component.html
index 16f93da003ef45253778b21864f45a82588fee1a..7fab8209cb2ce3ec74a2982dbfec41e47c310268 100644
--- a/client/src/app/portal/containers/portal-home.component.html
+++ b/client/src/app/portal/containers/portal-home.component.html
@@ -3,6 +3,8 @@
         [links]="links"
         [isAuthenticated]="isAuthenticated | async"
         [userProfile]="userProfile | async"
+        [baseHref]="getBaseHref()"
+        [authenticationEnabled]="getAuthenticationEnabled()"
         (login)="login()"
         (logout)="logout()"
         (openEditProfile)="openEditProfile()">
diff --git a/client/src/app/portal/containers/portal-home.component.spec.ts b/client/src/app/portal/containers/portal-home.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6d707e36f09a66596196c426ec4fc632bcbf1ddd
--- /dev/null
+++ b/client/src/app/portal/containers/portal-home.component.spec.ts
@@ -0,0 +1,88 @@
+import { Component, Input } from '@angular/core';
+import { TestBed, waitForAsync, ComponentFixture  } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { provideMockStore, MockStore } from '@ngrx/store/testing';
+
+import { AppConfigService } from 'src/app/app-config.service';
+import { UserProfile } from 'src/app/auth/user-profile.model';
+import { Instance } from 'src/app/metamodel/models';
+import * as authActions from 'src/app/auth/auth.actions';
+import { PortalHomeComponent } from './portal-home.component';
+
+describe('PortalHomeComponent', () => {
+    @Component({ selector: 'app-navbar', template: '' })
+    class NavbarStubComponent {
+        @Input() links: {label: string, icon: string, routerLink: string}[];
+        @Input() isAuthenticated: boolean;
+        @Input() userProfile: UserProfile = null;
+        @Input() baseHref: string;
+        @Input() authenticationEnabled: boolean;
+    }
+
+    @Component({ selector: 'app-instance-card', template: '' })
+    class InstanceCardStubComponent {
+        @Input() instance: Instance;
+    }
+
+    let component: PortalHomeComponent;
+    let fixture: ComponentFixture<PortalHomeComponent>;
+    let store: MockStore;
+    let config: AppConfigService
+
+    beforeEach(waitForAsync(() => {
+        TestBed.configureTestingModule({
+            imports: [
+                RouterTestingModule
+            ],
+            declarations: [
+                PortalHomeComponent,
+                NavbarStubComponent,
+                InstanceCardStubComponent
+            ],
+            providers: [
+                provideMockStore({ }),
+                AppConfigService
+            ]
+        }).compileComponents();
+        fixture = TestBed.createComponent(PortalHomeComponent);
+        component = fixture.componentInstance;
+        store = TestBed.inject(MockStore);
+        config = TestBed.inject(AppConfigService);
+    }));
+
+    it('should create the portal home component', () => {
+        expect(component).toBeDefined();
+    });
+
+    it('getBaseHref() should give base href config key value', () => {
+        config.baseHref = '/my-project';
+        expect(component.getBaseHref()).toBe('/my-project');
+    });
+
+    it('authenticationEnabled() should give authentication enabled config key value', () => {
+        config.authenticationEnabled = true;
+        expect(component.getAuthenticationEnabled()).toBeTruthy();
+    });
+
+    it('login() should dispatch login action', () => {
+        const spy = jest.spyOn(store, 'dispatch');
+        component.login();
+        expect(spy).toHaveBeenCalledTimes(1);
+        expect(spy).toHaveBeenCalledWith(authActions.login());
+    });
+
+    it('logout() should dispatch logout action', () => {
+        const spy = jest.spyOn(store, 'dispatch');
+        component.logout();
+        expect(spy).toHaveBeenCalledTimes(1);
+        expect(spy).toHaveBeenCalledWith(authActions.logout());
+    });
+
+    it('openEditProfile() should dispatch open edit profile action', () => {
+        const spy = jest.spyOn(store, 'dispatch');
+        component.openEditProfile();
+        expect(spy).toHaveBeenCalledTimes(1);
+        expect(spy).toHaveBeenCalledWith(authActions.openEditProfile());
+    });
+});
diff --git a/client/src/app/portal/containers/portal-home.component.ts b/client/src/app/portal/containers/portal-home.component.ts
index 78b1b7908034b394c7921731a610dbea31e8ac31..4405a97255d7be0a78375e525c845e425bf657c0 100644
--- a/client/src/app/portal/containers/portal-home.component.ts
+++ b/client/src/app/portal/containers/portal-home.component.ts
@@ -7,8 +7,8 @@
  * file that was distributed with this source code.
  */
 
-import { Component, OnInit } from '@angular/core';
-import { Observable } from 'rxjs';
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Observable, Subscription } from 'rxjs';
 import { Store } from '@ngrx/store';
 
 import { UserProfile } from 'src/app/auth/user-profile.model';
@@ -17,6 +17,7 @@ import * as authActions from 'src/app/auth/auth.actions';
 import * as authSelector from 'src/app/auth/auth.selector';
 import * as instanceActions from 'src/app/metamodel/actions/instance.actions';
 import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector';
+import { AppConfigService } from 'src/app/app-config.service';
 
 @Component({
     selector: 'app-portal-home',
@@ -28,10 +29,9 @@ import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector
  *
  * @implements OnInit
  */
-export class PortalHomeComponent implements OnInit {
+export class PortalHomeComponent implements OnInit, OnDestroy {
     public links = [
-        { label: 'Home', icon: 'fas fa-home', routerLink: '/portal-home' },
-        { label: 'Admin', icon: 'fas fa-tools', routerLink: '/admin' }
+        { label: 'Home', icon: 'fas fa-home', routerLink: '/portal' }
     ];
     public isAuthenticated: Observable<boolean>;
     public userProfile: Observable<UserProfile>;
@@ -40,7 +40,9 @@ export class PortalHomeComponent implements OnInit {
     public instanceListIsLoaded: Observable<boolean>;
     public instanceList: Observable<Instance[]>;
 
-    constructor(private store: Store<{ }>) {
+    public userRolesSubscription: Subscription;
+
+    constructor(private store: Store<{ }>, private config: AppConfigService) {
         this.isAuthenticated = store.select(authSelector.selectIsAuthenticated);
         this.userProfile = store.select(authSelector.selectUserProfile);
         this.userRoles = store.select(authSelector.selectUserRoles);
@@ -51,8 +53,26 @@ export class PortalHomeComponent implements OnInit {
 
     ngOnInit() {
         this.store.dispatch(instanceActions.loadInstanceList());
+        const adminLink = { label: 'Admin', icon: 'fas fa-tools', routerLink: '/admin' };
+        if (!this.config.authenticationEnabled) {
+            this.links.push(adminLink);
+        } else {
+            this.userRolesSubscription = this.userRoles.subscribe(userRoles => {
+                if (userRoles.includes(this.config.adminRole)) {
+                    this.links.push(adminLink);
+                }
+            });
+        }
+    }
+
+    getBaseHref() {
+        return this.config.baseHref;
     }
 
+    getAuthenticationEnabled() {
+        return this.config.authenticationEnabled;
+    }
+    
     login(): void {
         this.store.dispatch(authActions.login());
     }
@@ -64,4 +84,8 @@ export class PortalHomeComponent implements OnInit {
     openEditProfile(): void {
         this.store.dispatch(authActions.openEditProfile());
     }
+
+    ngOnDestroy() {
+        if (this.userRolesSubscription) this.userRolesSubscription.unsubscribe();
+    }
 }
diff --git a/client/src/app/portal/portal-routing.module.ts b/client/src/app/portal/portal-routing.module.ts
index ae68f942c0b7732c57bbb5e7b68096cd16c1032b..3874af69e7b07bd30358857ec40196b68681f2f5 100644
--- a/client/src/app/portal/portal-routing.module.ts
+++ b/client/src/app/portal/portal-routing.module.ts
@@ -13,11 +13,12 @@ import { RouterModule, Routes } from '@angular/router';
 import { PortalHomeComponent } from './containers/portal-home.component';
 
 const routes: Routes = [
-    { path: 'portal-home', component: PortalHomeComponent }
+    { path: '', redirectTo: 'home', pathMatch: 'full' },
+    { path: 'home', component: PortalHomeComponent }
 ];
 
 @NgModule({
-    imports: [RouterModule.forRoot(routes)],
+    imports: [RouterModule.forChild(routes)],
     exports: [RouterModule]
 })
 export class PortalRoutingModule { }
diff --git a/client/src/app/shared/components/navbar.component.ts b/client/src/app/shared/components/navbar.component.ts
index c88f742045052debb35000bb6da163ff9f150e2f..733e55789e7d5b65bea006491b717cfdd758574f 100644
--- a/client/src/app/shared/components/navbar.component.ts
+++ b/client/src/app/shared/components/navbar.component.ts
@@ -10,7 +10,6 @@
 import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
 
 import { UserProfile } from 'src/app/auth/user-profile.model';
-import { environment } from 'src/environments/environment'
 
 @Component({
     selector: 'app-navbar',
@@ -22,10 +21,9 @@ export class NavbarComponent {
     @Input() links: {label: string, icon: string, routerLink: string}[];
     @Input() isAuthenticated: boolean;
     @Input() userProfile: UserProfile = null;
+    @Input() baseHref: string;
+    @Input() authenticationEnabled: boolean;
     @Output() login: EventEmitter<any> = new EventEmitter();
     @Output() logout: EventEmitter<any> = new EventEmitter();
     @Output() openEditProfile: EventEmitter<any> = new EventEmitter();
-    
-    baseHref: string = environment.baseHref;
-    authenticationEnabled: boolean = environment.authenticationEnabled;
 }
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index b402511e14ee743e933a76b564ed0d8110bf17d4..4a68bd0d8d84a6118dfd4b7f0b13a6059ba385b9 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -12,7 +12,6 @@ import { CommonModule } from '@angular/common';
 import { RouterModule } from '@angular/router';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 
-import { ToastrModule } from 'ngx-toastr';
 import { CollapseModule } from 'ngx-bootstrap/collapse';
 import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
 import { ModalModule } from 'ngx-bootstrap/modal';
@@ -38,7 +37,6 @@ import { sharedPipes } from './pipes';
         RouterModule,
         FormsModule,
         ReactiveFormsModule,
-        ToastrModule.forRoot(),
         CollapseModule.forRoot(),
         BsDropdownModule.forRoot(),
         ModalModule.forRoot(),
@@ -55,7 +53,6 @@ import { sharedPipes } from './pipes';
         CommonModule,
         FormsModule,
         ReactiveFormsModule,
-        ToastrModule,
         CollapseModule,
         BsDropdownModule,
         ModalModule,
diff --git a/client/src/app/shared/utils.ts b/client/src/app/shared/utils.ts
index d5250e7ad656835ccfe3b3e758efbe9b0614cca0..b43ae07dacaf2fa56684636c8b8478909c2ff2e2 100644
--- a/client/src/app/shared/utils.ts
+++ b/client/src/app/shared/utils.ts
@@ -1,5 +1,3 @@
-import { environment } from 'src/environments/environment';
-
 /**
  * Returns strict url address.
  *
@@ -8,12 +6,12 @@ import { environment } from 'src/environments/environment';
  * @example
  * const url: string = getHost() + '/following-url/';
  */
-export const getHost = (): string => {
-    if (!environment.apiUrl.startsWith('http')) {
+export const getHost = (apiUrl: string): string => {
+    if (!apiUrl.startsWith('http')) {
         const url = window.location;
-        return url.protocol + '//' + url.host + environment.apiUrl;
+        return url.protocol + '//' + url.host + apiUrl;
     }
-    return environment.apiUrl;
+    return apiUrl;
 }
 
 /**
diff --git a/client/src/assets/app.config.json b/client/src/assets/app.config.json
new file mode 100644
index 0000000000000000000000000000000000000000..0270c50c94d4db9f30d2c118f369f75b0c01884f
--- /dev/null
+++ b/client/src/assets/app.config.json
@@ -0,0 +1,10 @@
+{
+    "apiUrl": "http://localhost:8080",
+    "servicesUrl": "http://localhost:5000",
+    "baseHref": "/",
+    "authenticationEnabled": true,
+    "ssoAuthUrl": "http://localhost:8180/auth",
+    "ssoRealm": "anis",
+    "ssoClientId": "anis-client",
+    "adminRole": "anis_admin"
+}
\ No newline at end of file
diff --git a/client/src/environments/environment.prod.ts b/client/src/environments/environment.prod.ts
index 5b72becdf6cfb8b2cfbb20698a4971ee4a6c0a0a..5d0833162027e2147e99e72a1a94bb7d3cb62843 100644
--- a/client/src/environments/environment.prod.ts
+++ b/client/src/environments/environment.prod.ts
@@ -1,12 +1,3 @@
 export const environment = {
-    production: true,
-    apiUrl: '/server',
-    servicesUrl: '/services',
-    baseHref: '/',
-    authenticationEnabled: true,
-    ssoAuthUrl: 'https://keycloak.lam.fr/auth/',
-    ssoRealm: 'anis',
-    ssoClientId: 'anis-dev',
-    ssoLoginRedirectUri: '/',
-    ssoLogoutRedirectUri: '/'
+    production: true
 };
diff --git a/client/src/environments/environment.ts b/client/src/environments/environment.ts
index f2825d0d24a5bc4607c135ef2e9b56eac4128dc9..458476a4df52c64d1a2abee43c6cb91d87abad77 100644
--- a/client/src/environments/environment.ts
+++ b/client/src/environments/environment.ts
@@ -3,16 +3,7 @@
 // The list of file replacements can be found in `angular.json`.
 
 export const environment = {
-    production: false,
-    apiUrl: 'http://localhost:8080',
-    servicesUrl: 'http://localhost:5000',
-    baseHref: '/',
-    authenticationEnabled: true,
-    ssoAuthUrl: 'http://localhost:8180/auth',
-    ssoRealm: 'anis',
-    ssoClientId: 'anis-client',
-    ssoLoginRedirectUri: '/',
-    ssoLogoutRedirectUri: '/'
+    production: false
 };
 
 /*
diff --git a/client/src/test.ts b/client/src/test.ts
index 2042356408ff9a673a6ca98b4eb2b9cb7e96f1a5..67041854268d4672d4ca9c764a16321e05c57a58 100644
--- a/client/src/test.ts
+++ b/client/src/test.ts
@@ -1,25 +1,23 @@
-// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+import 'jest-preset-angular/setup-jest';
 
-import 'zone.js/testing';
-import { getTestBed } from '@angular/core/testing';
-import {
-  BrowserDynamicTestingModule,
-  platformBrowserDynamicTesting
-} from '@angular/platform-browser-dynamic/testing';
+Object.defineProperty(window, 'CSS', { value: null });
+Object.defineProperty(window, 'getComputedStyle', {
+    value: () => {
+        return {
+            display: 'none',
+            appearance: ['-webkit-appearance'],
+        };
+    },
+});
 
-declare const require: {
-  context(path: string, deep?: boolean, filter?: RegExp): {
-    keys(): string[];
-    <T>(id: string): T;
-  };
-};
-
-// First, initialize the Angular testing environment.
-getTestBed().initTestEnvironment(
-  BrowserDynamicTestingModule,
-  platformBrowserDynamicTesting()
-);
-// Then we find all the tests.
-const context = require.context('./', true, /\.spec\.ts$/);
-// And load the modules.
-context.keys().map(context);
+Object.defineProperty(document, 'doctype', {
+    value: '<!DOCTYPE html>',
+});
+Object.defineProperty(document.body.style, 'transform', {
+    value: () => {
+        return {
+            enumerable: true,
+            configurable: true,
+        };
+    },
+});
diff --git a/client/tsconfig.json b/client/tsconfig.json
index 824b803cca255aed75c72380255ffcc15fe1c85b..84c435c4bc3d43f702dc44877cea4216a255f5c0 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -1,4 +1,3 @@
-/* To learn more about this file see: https://angular.io/config/tsconfig. */
 {
   "compileOnSave": false,
   "compilerOptions": {
diff --git a/client/tsconfig.spec.json b/client/tsconfig.spec.json
index 092345b02e807773164dba19f2b4d1f4ab869f67..8f2c1326542880fa39c0d4cf9e6600f06b0481a1 100644
--- a/client/tsconfig.spec.json
+++ b/client/tsconfig.spec.json
@@ -1,18 +1,14 @@
 /* To learn more about this file see: https://angular.io/config/tsconfig. */
 {
-  "extends": "./tsconfig.json",
-  "compilerOptions": {
-    "outDir": "./out-tsc/spec",
-    "types": [
-      "jasmine"
-    ]
-  },
-  "files": [
-    "src/test.ts",
-    "src/polyfills.ts"
-  ],
-  "include": [
-    "src/**/*.spec.ts",
-    "src/**/*.d.ts"
-  ]
+    "extends": "./tsconfig.json",
+    "compilerOptions": {
+        "outDir": "./out-tsc/spec",
+        "types": [
+            "jest"
+        ],
+        "esModuleInterop": true,
+        "emitDecoratorMetadata": true
+    },
+    "files": ["src/test.ts", "src/polyfills.ts"],
+    "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
 }
diff --git a/client/yarn.lock b/client/yarn.lock
index 2a005f9358ac87f05bedd31cb41ab6f7bf727db2..dfc67f9ae6f0b25094479ddb02882daaaa9a9099 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -256,6 +256,27 @@
     semver "^6.3.0"
     source-map "^0.5.0"
 
+"@babel/core@^7.1.0", "@babel/core@^7.7.2":
+  version "7.14.8"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.8.tgz#20cdf7c84b5d86d83fac8710a8bc605a7ba3f010"
+  integrity sha512-/AtaeEhT6ErpDhInbXmjHcUQXH0L0TEgscfcxk1qbOvLuKCa5aZT0SOOtDKFY96/CLROwbLSKyFor6idgNaU4Q==
+  dependencies:
+    "@babel/code-frame" "^7.14.5"
+    "@babel/generator" "^7.14.8"
+    "@babel/helper-compilation-targets" "^7.14.5"
+    "@babel/helper-module-transforms" "^7.14.8"
+    "@babel/helpers" "^7.14.8"
+    "@babel/parser" "^7.14.8"
+    "@babel/template" "^7.14.5"
+    "@babel/traverse" "^7.14.8"
+    "@babel/types" "^7.14.8"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.2"
+    json5 "^2.1.2"
+    semver "^6.3.0"
+    source-map "^0.5.0"
+
 "@babel/core@^7.7.5", "@babel/core@^7.8.6":
   version "7.14.6"
   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.6.tgz#e0814ec1a950032ff16c13a2721de39a8416fcab"
@@ -295,6 +316,15 @@
     jsesc "^2.5.1"
     source-map "^0.5.0"
 
+"@babel/generator@^7.14.8", "@babel/generator@^7.7.2":
+  version "7.14.8"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.8.tgz#bf86fd6af96cf3b74395a8ca409515f89423e070"
+  integrity sha512-cYDUpvIzhBVnMzRoY1fkSEhK/HmwEVwlyULYgn/tMQYd6Obag3ylCjONle3gdErfXBW61SVTlR9QR7uWlgeIkg==
+  dependencies:
+    "@babel/types" "^7.14.8"
+    jsesc "^2.5.1"
+    source-map "^0.5.0"
+
 "@babel/helper-annotate-as-pure@^7.14.5":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz#7bf478ec3b71726d56a8ca5775b046fc29879e61"
@@ -412,6 +442,20 @@
     "@babel/traverse" "^7.14.5"
     "@babel/types" "^7.14.5"
 
+"@babel/helper-module-transforms@^7.14.8":
+  version "7.14.8"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.8.tgz#d4279f7e3fd5f4d5d342d833af36d4dd87d7dc49"
+  integrity sha512-RyE+NFOjXn5A9YU1dkpeBaduagTlZ0+fccnIcAGbv1KGUlReBj7utF7oEth8IdIBQPcux0DDgW5MFBH2xu9KcA==
+  dependencies:
+    "@babel/helper-module-imports" "^7.14.5"
+    "@babel/helper-replace-supers" "^7.14.5"
+    "@babel/helper-simple-access" "^7.14.8"
+    "@babel/helper-split-export-declaration" "^7.14.5"
+    "@babel/helper-validator-identifier" "^7.14.8"
+    "@babel/template" "^7.14.5"
+    "@babel/traverse" "^7.14.8"
+    "@babel/types" "^7.14.8"
+
 "@babel/helper-optimise-call-expression@^7.14.5":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz#f27395a8619e0665b3f0364cddb41c25d71b499c"
@@ -450,6 +494,13 @@
   dependencies:
     "@babel/types" "^7.14.5"
 
+"@babel/helper-simple-access@^7.14.8":
+  version "7.14.8"
+  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz#82e1fec0644a7e775c74d305f212c39f8fe73924"
+  integrity sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg==
+  dependencies:
+    "@babel/types" "^7.14.8"
+
 "@babel/helper-skip-transparent-expression-wrappers@^7.14.5":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.14.5.tgz#96f486ac050ca9f44b009fbe5b7d394cab3a0ee4"
@@ -469,6 +520,11 @@
   resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8"
   integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==
 
+"@babel/helper-validator-identifier@^7.14.8":
+  version "7.14.8"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.8.tgz#32be33a756f29e278a0d644fa08a2c9e0f88a34c"
+  integrity sha512-ZGy6/XQjllhYQrNw/3zfWRwZCTVSiBLZ9DHVZxn9n2gip/7ab8mv2TWlKPIBk26RwedCBoWdjLmn+t9na2Gcow==
+
 "@babel/helper-validator-option@^7.12.17", "@babel/helper-validator-option@^7.14.5":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3"
@@ -493,6 +549,15 @@
     "@babel/traverse" "^7.14.5"
     "@babel/types" "^7.14.5"
 
+"@babel/helpers@^7.14.8":
+  version "7.14.8"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.8.tgz#839f88f463025886cff7f85a35297007e2da1b77"
+  integrity sha512-ZRDmI56pnV+p1dH6d+UN6GINGz7Krps3+270qqI9UJ4wxYThfAIcI5i7j5vXC4FJ3Wap+S9qcebxeYiqn87DZw==
+  dependencies:
+    "@babel/template" "^7.14.5"
+    "@babel/traverse" "^7.14.8"
+    "@babel/types" "^7.14.8"
+
 "@babel/highlight@^7.14.5":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9"
@@ -502,6 +567,11 @@
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
+"@babel/parser@^7.1.0", "@babel/parser@^7.14.8", "@babel/parser@^7.7.2":
+  version "7.14.8"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.8.tgz#66fd41666b2d7b840bd5ace7f7416d5ac60208d4"
+  integrity sha512-syoCQFOoo/fzkWDeM0dLEZi5xqurb5vuyzwIMNZRNun+N/9A4cUZeQaE7dTrB8jGaKuJRBtEOajtnmw0I5hvvA==
+
 "@babel/parser@^7.12.13", "@babel/parser@^7.14.3", "@babel/parser@^7.14.5", "@babel/parser@^7.14.6":
   version "7.14.6"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.6.tgz#d85cc68ca3cac84eae384c06f032921f5227f4b2"
@@ -651,7 +721,14 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-class-properties@^7.12.13":
+"@babel/plugin-syntax-bigint@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea"
+  integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-class-properties@^7.12.13", "@babel/plugin-syntax-class-properties@^7.8.3":
   version "7.12.13"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10"
   integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==
@@ -679,6 +756,13 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
+"@babel/plugin-syntax-import-meta@^7.8.3":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"
+  integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+
 "@babel/plugin-syntax-json-strings@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
@@ -686,7 +770,7 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-logical-assignment-operators@^7.10.4":
+"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699"
   integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==
@@ -700,7 +784,7 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-numeric-separator@^7.10.4":
+"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97"
   integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==
@@ -735,13 +819,20 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.14.5"
 
-"@babel/plugin-syntax-top-level-await@^7.12.13":
+"@babel/plugin-syntax-top-level-await@^7.12.13", "@babel/plugin-syntax-top-level-await@^7.8.3":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c"
   integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.14.5"
 
+"@babel/plugin-syntax-typescript@^7.7.2":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.14.5.tgz#b82c6ce471b165b5ce420cf92914d6fb46225716"
+  integrity sha512-u6OXzDaIXjEstBRRoBCQ/uKQKlbuaeE5in0RvWdA4pN6AhqxTIwUsnHPU1CFZA/amYObMsuWhYfRl3Ch90HD0Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.14.5"
+
 "@babel/plugin-transform-arrow-functions@^7.13.0":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.14.5.tgz#f7187d9588a768dd080bf4c9ffe117ea62f7862a"
@@ -1124,7 +1215,7 @@
     "@babel/parser" "^7.12.13"
     "@babel/types" "^7.12.13"
 
-"@babel/template@^7.12.13", "@babel/template@^7.14.5":
+"@babel/template@^7.12.13", "@babel/template@^7.14.5", "@babel/template@^7.3.3":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4"
   integrity sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==
@@ -1133,6 +1224,21 @@
     "@babel/parser" "^7.14.5"
     "@babel/types" "^7.14.5"
 
+"@babel/traverse@^7.1.0", "@babel/traverse@^7.14.8", "@babel/traverse@^7.7.2":
+  version "7.14.8"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.8.tgz#c0253f02677c5de1a8ff9df6b0aacbec7da1a8ce"
+  integrity sha512-kexHhzCljJcFNn1KYAQ6A5wxMRzq9ebYpEDV4+WdNyr3i7O44tanbDOR/xjiG2F3sllan+LgwK+7OMk0EmydHg==
+  dependencies:
+    "@babel/code-frame" "^7.14.5"
+    "@babel/generator" "^7.14.8"
+    "@babel/helper-function-name" "^7.14.5"
+    "@babel/helper-hoist-variables" "^7.14.5"
+    "@babel/helper-split-export-declaration" "^7.14.5"
+    "@babel/parser" "^7.14.8"
+    "@babel/types" "^7.14.8"
+    debug "^4.1.0"
+    globals "^11.1.0"
+
 "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.2", "@babel/traverse@^7.14.5":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.5.tgz#c111b0f58afab4fea3d3385a406f692748c59870"
@@ -1148,6 +1254,14 @@
     debug "^4.1.0"
     globals "^11.1.0"
 
+"@babel/types@^7.0.0", "@babel/types@^7.14.8", "@babel/types@^7.3.0", "@babel/types@^7.3.3":
+  version "7.14.8"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.8.tgz#38109de8fcadc06415fbd9b74df0065d4d41c728"
+  integrity sha512-iob4soQa7dZw8nodR/KlOQkPh9S4I8RwCxwRIFuiMRYjOzH/KJzdUfDgz6cGi5dDaclXF4P2PAhCdrBJNIg68Q==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.14.8"
+    to-fast-properties "^2.0.0"
+
 "@babel/types@^7.12.13", "@babel/types@^7.14.2", "@babel/types@^7.14.5", "@babel/types@^7.4.4", "@babel/types@^7.8.6":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.5.tgz#3bb997ba829a2104cedb20689c4a5b8121d383ff"
@@ -1156,6 +1270,11 @@
     "@babel/helper-validator-identifier" "^7.14.5"
     to-fast-properties "^2.0.0"
 
+"@bcoe/v8-coverage@^0.2.3":
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
+  integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
+
 "@csstools/convert-colors@^1.4.0":
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
@@ -1171,11 +1290,202 @@
   resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz#c36ffa64a2a239bf948541a97b6ae8d729e09a9a"
   integrity sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w==
 
+"@istanbuljs/load-nyc-config@^1.0.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
+  integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==
+  dependencies:
+    camelcase "^5.3.1"
+    find-up "^4.1.0"
+    get-package-type "^0.1.0"
+    js-yaml "^3.13.1"
+    resolve-from "^5.0.0"
+
 "@istanbuljs/schema@^0.1.2":
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
   integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
 
+"@jest/console@^27.0.6":
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.0.6.tgz#3eb72ea80897495c3d73dd97aab7f26770e2260f"
+  integrity sha512-fMlIBocSHPZ3JxgWiDNW/KPj6s+YRd0hicb33IrmelCcjXo/pXPwvuiKFmZz+XuqI/1u7nbUK10zSsWL/1aegg==
+  dependencies:
+    "@jest/types" "^27.0.6"
+    "@types/node" "*"
+    chalk "^4.0.0"
+    jest-message-util "^27.0.6"
+    jest-util "^27.0.6"
+    slash "^3.0.0"
+
+"@jest/core@^27.0.6":
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.0.6.tgz#c5f642727a0b3bf0f37c4b46c675372d0978d4a1"
+  integrity sha512-SsYBm3yhqOn5ZLJCtccaBcvD/ccTLCeuDv8U41WJH/V1MW5eKUkeMHT9U+Pw/v1m1AIWlnIW/eM2XzQr0rEmow==
+  dependencies:
+    "@jest/console" "^27.0.6"
+    "@jest/reporters" "^27.0.6"
+    "@jest/test-result" "^27.0.6"
+    "@jest/transform" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    "@types/node" "*"
+    ansi-escapes "^4.2.1"
+    chalk "^4.0.0"
+    emittery "^0.8.1"
+    exit "^0.1.2"
+    graceful-fs "^4.2.4"
+    jest-changed-files "^27.0.6"
+    jest-config "^27.0.6"
+    jest-haste-map "^27.0.6"
+    jest-message-util "^27.0.6"
+    jest-regex-util "^27.0.6"
+    jest-resolve "^27.0.6"
+    jest-resolve-dependencies "^27.0.6"
+    jest-runner "^27.0.6"
+    jest-runtime "^27.0.6"
+    jest-snapshot "^27.0.6"
+    jest-util "^27.0.6"
+    jest-validate "^27.0.6"
+    jest-watcher "^27.0.6"
+    micromatch "^4.0.4"
+    p-each-series "^2.1.0"
+    rimraf "^3.0.0"
+    slash "^3.0.0"
+    strip-ansi "^6.0.0"
+
+"@jest/environment@^27.0.6":
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.0.6.tgz#ee293fe996db01d7d663b8108fa0e1ff436219d2"
+  integrity sha512-4XywtdhwZwCpPJ/qfAkqExRsERW+UaoSRStSHCCiQTUpoYdLukj+YJbQSFrZjhlUDRZeNiU9SFH0u7iNimdiIg==
+  dependencies:
+    "@jest/fake-timers" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    "@types/node" "*"
+    jest-mock "^27.0.6"
+
+"@jest/fake-timers@^27.0.6":
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.0.6.tgz#cbad52f3fe6abe30e7acb8cd5fa3466b9588e3df"
+  integrity sha512-sqd+xTWtZ94l3yWDKnRTdvTeZ+A/V7SSKrxsrOKSqdyddb9CeNRF8fbhAU0D7ZJBpTTW2nbp6MftmKJDZfW2LQ==
+  dependencies:
+    "@jest/types" "^27.0.6"
+    "@sinonjs/fake-timers" "^7.0.2"
+    "@types/node" "*"
+    jest-message-util "^27.0.6"
+    jest-mock "^27.0.6"
+    jest-util "^27.0.6"
+
+"@jest/globals@^27.0.6":
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.0.6.tgz#48e3903f99a4650673d8657334d13c9caf0e8f82"
+  integrity sha512-DdTGCP606rh9bjkdQ7VvChV18iS7q0IMJVP1piwTWyWskol4iqcVwthZmoJEf7obE1nc34OpIyoVGPeqLC+ryw==
+  dependencies:
+    "@jest/environment" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    expect "^27.0.6"
+
+"@jest/reporters@^27.0.6":
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.0.6.tgz#91e7f2d98c002ad5df94d5b5167c1eb0b9fd5b00"
+  integrity sha512-TIkBt09Cb2gptji3yJXb3EE+eVltW6BjO7frO7NEfjI9vSIYoISi5R3aI3KpEDXlB1xwB+97NXIqz84qYeYsfA==
+  dependencies:
+    "@bcoe/v8-coverage" "^0.2.3"
+    "@jest/console" "^27.0.6"
+    "@jest/test-result" "^27.0.6"
+    "@jest/transform" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    chalk "^4.0.0"
+    collect-v8-coverage "^1.0.0"
+    exit "^0.1.2"
+    glob "^7.1.2"
+    graceful-fs "^4.2.4"
+    istanbul-lib-coverage "^3.0.0"
+    istanbul-lib-instrument "^4.0.3"
+    istanbul-lib-report "^3.0.0"
+    istanbul-lib-source-maps "^4.0.0"
+    istanbul-reports "^3.0.2"
+    jest-haste-map "^27.0.6"
+    jest-resolve "^27.0.6"
+    jest-util "^27.0.6"
+    jest-worker "^27.0.6"
+    slash "^3.0.0"
+    source-map "^0.6.0"
+    string-length "^4.0.1"
+    terminal-link "^2.0.0"
+    v8-to-istanbul "^8.0.0"
+
+"@jest/source-map@^27.0.6":
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.0.6.tgz#be9e9b93565d49b0548b86e232092491fb60551f"
+  integrity sha512-Fek4mi5KQrqmlY07T23JRi0e7Z9bXTOOD86V/uS0EIW4PClvPDqZOyFlLpNJheS6QI0FNX1CgmPjtJ4EA/2M+g==
+  dependencies:
+    callsites "^3.0.0"
+    graceful-fs "^4.2.4"
+    source-map "^0.6.0"
+
+"@jest/test-result@^27.0.6":
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.0.6.tgz#3fa42015a14e4fdede6acd042ce98c7f36627051"
+  integrity sha512-ja/pBOMTufjX4JLEauLxE3LQBPaI2YjGFtXexRAjt1I/MbfNlMx0sytSX3tn5hSLzQsR3Qy2rd0hc1BWojtj9w==
+  dependencies:
+    "@jest/console" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    "@types/istanbul-lib-coverage" "^2.0.0"
+    collect-v8-coverage "^1.0.0"
+
+"@jest/test-sequencer@^27.0.6":
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.0.6.tgz#80a913ed7a1130545b1cd777ff2735dd3af5d34b"
+  integrity sha512-bISzNIApazYOlTHDum9PwW22NOyDa6VI31n6JucpjTVM0jD6JDgqEZ9+yn575nDdPF0+4csYDxNNW13NvFQGZA==
+  dependencies:
+    "@jest/test-result" "^27.0.6"
+    graceful-fs "^4.2.4"
+    jest-haste-map "^27.0.6"
+    jest-runtime "^27.0.6"
+
+"@jest/transform@^27.0.6":
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.0.6.tgz#189ad7107413208f7600f4719f81dd2f7278cc95"
+  integrity sha512-rj5Dw+mtIcntAUnMlW/Vju5mr73u8yg+irnHwzgtgoeI6cCPOvUwQ0D1uQtc/APmWgvRweEb1g05pkUpxH3iCA==
+  dependencies:
+    "@babel/core" "^7.1.0"
+    "@jest/types" "^27.0.6"
+    babel-plugin-istanbul "^6.0.0"
+    chalk "^4.0.0"
+    convert-source-map "^1.4.0"
+    fast-json-stable-stringify "^2.0.0"
+    graceful-fs "^4.2.4"
+    jest-haste-map "^27.0.6"
+    jest-regex-util "^27.0.6"
+    jest-util "^27.0.6"
+    micromatch "^4.0.4"
+    pirates "^4.0.1"
+    slash "^3.0.0"
+    source-map "^0.6.1"
+    write-file-atomic "^3.0.0"
+
+"@jest/types@^26.6.2":
+  version "26.6.2"
+  resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e"
+  integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==
+  dependencies:
+    "@types/istanbul-lib-coverage" "^2.0.0"
+    "@types/istanbul-reports" "^3.0.0"
+    "@types/node" "*"
+    "@types/yargs" "^15.0.0"
+    chalk "^4.0.0"
+
+"@jest/types@^27.0.6":
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.0.6.tgz#9a992bc517e0c49f035938b8549719c2de40706b"
+  integrity sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==
+  dependencies:
+    "@types/istanbul-lib-coverage" "^2.0.0"
+    "@types/istanbul-reports" "^3.0.0"
+    "@types/node" "*"
+    "@types/yargs" "^16.0.0"
+    chalk "^4.0.0"
+
 "@jsdevtools/coverage-istanbul-loader@3.0.5":
   version "3.0.5"
   resolved "https://registry.yarnpkg.com/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz#2a4bc65d0271df8d4435982db4af35d81754ee26"
@@ -1319,6 +1629,20 @@
     "@angular-devkit/schematics" "12.0.4"
     jsonc-parser "3.0.0"
 
+"@sinonjs/commons@^1.7.0":
+  version "1.8.3"
+  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
+  integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==
+  dependencies:
+    type-detect "4.0.8"
+
+"@sinonjs/fake-timers@^7.0.2":
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz#2524eae70c4910edccf99b2f4e6efc5894aff7b5"
+  integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==
+  dependencies:
+    "@sinonjs/commons" "^1.7.0"
+
 "@tootallnate/once@1":
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@@ -1329,20 +1653,254 @@
   resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.1.1.tgz#3348564048e7a2d7398c935d466c0414ebb6a669"
   integrity sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow==
 
-"@types/component-emitter@^1.2.10":
-  version "1.2.10"
-  resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea"
-  integrity sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==
+"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14":
+  version "7.1.15"
+  resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.15.tgz#2ccfb1ad55a02c83f8e0ad327cbc332f55eb1024"
+  integrity sha512-bxlMKPDbY8x5h6HBwVzEOk2C8fb6SLfYQ5Jw3uBYuYF1lfWk/kbLd81la82vrIkBb0l+JdmrZaDikPrNxpS/Ew==
+  dependencies:
+    "@babel/parser" "^7.1.0"
+    "@babel/types" "^7.0.0"
+    "@types/babel__generator" "*"
+    "@types/babel__template" "*"
+    "@types/babel__traverse" "*"
 
-"@types/cookie@^0.4.0":
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108"
-  integrity sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==
+"@types/babel__generator@*":
+  version "7.6.3"
+  resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.3.tgz#f456b4b2ce79137f768aa130d2423d2f0ccfaba5"
+  integrity sha512-/GWCmzJWqV7diQW54smJZzWbSFf4QYtF71WCKhcx6Ru/tFyQIY2eiiITcCAeuPbNSvT9YCGkVMqqvSk2Z0mXiA==
+  dependencies:
+    "@babel/types" "^7.0.0"
+
+"@types/babel__template@*":
+  version "7.4.1"
+  resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969"
+  integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==
+  dependencies:
+    "@babel/parser" "^7.1.0"
+    "@babel/types" "^7.0.0"
 
-"@types/cors@^2.8.8":
-  version "2.8.10"
-  resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.10.tgz#61cc8469849e5bcdd0c7044122265c39cec10cf4"
-  integrity sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==
+"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6":
+  version "7.14.2"
+  resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43"
+  integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA==
+  dependencies:
+    "@babel/types" "^7.3.0"
+
+"@types/d3-array@^1":
+  version "1.2.9"
+  resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.9.tgz#c7dc78992cd8ca5c850243a265fd257ea56df1fa"
+  integrity sha512-E/7RgPr2ylT5dWG0CswMi9NpFcjIEDqLcUSBgNHe/EMahfqYaTx4zhcggG3khqoEB/leY4Vl6nTSbwLUPjXceA==
+
+"@types/d3-axis@^1":
+  version "1.0.16"
+  resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-1.0.16.tgz#93d7a28795c2f8b0e2fd550fcc4d29b7f174e693"
+  integrity sha512-p7085weOmo4W+DzlRRVC/7OI/jugaKbVa6WMQGCQscaMylcbuaVEGk7abJLNyGVFLeCBNrHTdDiqRGnzvL0nXQ==
+  dependencies:
+    "@types/d3-selection" "^1"
+
+"@types/d3-brush@^1":
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-1.1.5.tgz#c7cfb58decbfd53ad3e47f0376345e3640a68186"
+  integrity sha512-4zGkBafJf5zCsBtLtvDj/pNMo5X9+Ii/1hUz0GvQ+wEwelUBm2AbIDAzJnp2hLDFF307o0fhxmmocHclhXC+tw==
+  dependencies:
+    "@types/d3-selection" "^1"
+
+"@types/d3-chord@^1":
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-1.0.11.tgz#5760765db1b1a4b936c0d9355a821dde9dd25da2"
+  integrity sha512-0DdfJ//bxyW3G9Nefwq/LDgazSKNN8NU0lBT3Cza6uVuInC2awMNsAcv1oKyRFLn9z7kXClH5XjwpveZjuz2eg==
+
+"@types/d3-collection@*":
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/@types/d3-collection/-/d3-collection-1.0.10.tgz#bca161e336156968f267c077f7f2bfa8ff224e58"
+  integrity sha512-54Fdv8u5JbuXymtmXm2SYzi1x/Svt+jfWBU5junkhrCewL92VjqtCBDn97coBRVwVFmYNnVTNDyV8gQyPYfm+A==
+
+"@types/d3-color@^1":
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.4.2.tgz#944f281d04a0f06e134ea96adbb68303515b2784"
+  integrity sha512-fYtiVLBYy7VQX+Kx7wU/uOIkGQn8aAEY8oWMoyja3N4dLd8Yf6XgSIR/4yWvMuveNOH5VShnqCgRqqh/UNanBA==
+
+"@types/d3-contour@^1":
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-1.3.3.tgz#44529d498bbc1db78b195d75e1c9bb889edd647a"
+  integrity sha512-LxwmGIfVJIc1cKs7ZFRQ1FbtXpfH7QTXYRdMIJsFP71uCMdF6jJ0XZakYDX6Hn4yZkLf+7V8FgD34yCcok+5Ww==
+  dependencies:
+    "@types/d3-array" "^1"
+    "@types/geojson" "*"
+
+"@types/d3-dispatch@^1":
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-1.0.9.tgz#c5a180f1e251de853b399cfbfbb6dd7f8bf842ae"
+  integrity sha512-zJ44YgjqALmyps+II7b1mZLhrtfV/FOxw9owT87mrweGWcg+WK5oiJX2M3SYJ0XUAExBduarysfgbR11YxzojQ==
+
+"@types/d3-drag@^1":
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-1.2.5.tgz#0b1b852cb41577075aa625ae6149379ea6c34dfd"
+  integrity sha512-7NeTnfolst1Js3Vs7myctBkmJWu6DMI3k597AaHUX98saHjHWJ6vouT83UrpE+xfbSceHV+8A0JgxuwgqgmqWw==
+  dependencies:
+    "@types/d3-selection" "^1"
+
+"@types/d3-dsv@^1":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-1.2.1.tgz#1524fee9f19d689c2f76aa0e24e230762bf96994"
+  integrity sha512-LLmJmjiqp/fTNEdij5bIwUJ6P6TVNk5hKM9/uk5RPO2YNgEu9XvKO0dJ7Iqd3psEdmZN1m7gB1bOsjr4HmO2BA==
+
+"@types/d3-ease@^1":
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-1.0.10.tgz#09910e8558439b6038a7ed620650e510394ffa6d"
+  integrity sha512-fMFTCzd8DOwruE9zlu2O8ci5ct+U5jkGcDS+cH+HCidnJlDs0MZ+TuSVCFtEzh4E5MasItwy+HvgoFtxPHa5Cw==
+
+"@types/d3-fetch@^1":
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-1.2.2.tgz#b93bfe248b8b761af82f4dac57959c989f67da3e"
+  integrity sha512-rtFs92GugtV/NpiJQd0WsmGLcg52tIL0uF0bKbbJg231pR9JEb6HT4AUwrtuLq3lOeKdLBhsjV14qb0pMmd0Aw==
+  dependencies:
+    "@types/d3-dsv" "^1"
+
+"@types/d3-force@^1":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-1.2.4.tgz#6e274c72288c2db08fbdb8f5b87b9aa83e55a9e8"
+  integrity sha512-fkorLTKvt6AQbFBQwn4aq7h9rJ4c7ZVcPMGB8X6eFFveAyMZcv7t7m6wgF4Eg93rkPgPORU7sAho1QSHNcZu6w==
+
+"@types/d3-format@^1":
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.4.2.tgz#ea17bf559b71d9afd569ae9bfe4c544dab863baa"
+  integrity sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ==
+
+"@types/d3-geo@^1":
+  version "1.12.3"
+  resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-1.12.3.tgz#512ebe735cb1cdf5f87ad59608416e2e9e868c5a"
+  integrity sha512-yZbPb7/5DyL/pXkeOmZ7L5ySpuGr4H48t1cuALjnJy5sXQqmSSAYBiwa6Ya/XpWKX2rJqGDDubmh3nOaopOpeA==
+  dependencies:
+    "@types/geojson" "*"
+
+"@types/d3-hierarchy@^1":
+  version "1.1.8"
+  resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz#50657f420d565a06c0b950a4b82eee0a369f2dea"
+  integrity sha512-AbStKxNyWiMDQPGDguG2Kuhlq1Sv539pZSxYbx4UZeYkutpPwXCcgyiRrlV4YH64nIOsKx7XVnOMy9O7rJsXkg==
+
+"@types/d3-interpolate@^1":
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-1.4.2.tgz#88902a205f682773a517612299a44699285eed7b"
+  integrity sha512-ylycts6llFf8yAEs1tXzx2loxxzDZHseuhPokrqKprTQSTcD3JbJI1omZP1rphsELZO3Q+of3ff0ZS7+O6yVzg==
+  dependencies:
+    "@types/d3-color" "^1"
+
+"@types/d3-path@^1":
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.9.tgz#73526b150d14cd96e701597cbf346cfd1fd4a58c"
+  integrity sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ==
+
+"@types/d3-polygon@^1":
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-1.0.8.tgz#127ee83fccda5bf57384011da90f31367fea1530"
+  integrity sha512-1TOJPXCBJC9V3+K3tGbTqD/CsqLyv/YkTXAcwdsZzxqw5cvpdnCuDl42M4Dvi8XzMxZNCT9pL4ibrK2n4VmAcw==
+
+"@types/d3-quadtree@^1":
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-1.0.9.tgz#c7c3b795b5af06e5b043d1d34e754a434b3bae59"
+  integrity sha512-5E0OJJn2QVavITFEc1AQlI8gLcIoDZcTKOD3feKFckQVmFV4CXhqRFt83tYNVNIN4ZzRkjlAMavJa1ldMhf5rA==
+
+"@types/d3-random@^1":
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-1.1.3.tgz#8f7fdc23f92d1561e0694eb49567e8ab50537a19"
+  integrity sha512-XXR+ZbFCoOd4peXSMYJzwk0/elP37WWAzS/DG+90eilzVbUSsgKhBcWqylGWe+lA2ubgr7afWAOBaBxRgMUrBQ==
+
+"@types/d3-scale-chromatic@^1":
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-1.5.1.tgz#e2b7c3401e5c13809f831911eb820e444f4fc67a"
+  integrity sha512-7FtJYrmXTEWLykShjYhoGuDNR/Bda0+tstZMkFj4RRxUEryv16AGh3be21tqg84B6KfEwiZyEpBcTyPyU+GWjg==
+
+"@types/d3-scale@^2":
+  version "2.2.6"
+  resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-2.2.6.tgz#28540b4dfc99d978970e873e4138a6bea2ea6ab8"
+  integrity sha512-CHu34T5bGrJOeuhGxyiz9Xvaa9PlsIaQoOqjDg7zqeGj2x0rwPhGquiy03unigvcMxmvY0hEaAouT0LOFTLpIw==
+  dependencies:
+    "@types/d3-time" "^1"
+
+"@types/d3-selection@^1":
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.4.3.tgz#36928bbe64eb8e0bbcbaa01fb05c21ff6c71fa93"
+  integrity sha512-GjKQWVZO6Sa96HiKO6R93VBE8DUW+DDkFpIMf9vpY5S78qZTlRRSNUsHr/afDpF7TvLDV7VxrUFOWW7vdIlYkA==
+
+"@types/d3-shape@^1":
+  version "1.3.8"
+  resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.8.tgz#c3c15ec7436b4ce24e38de517586850f1fea8e89"
+  integrity sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg==
+  dependencies:
+    "@types/d3-path" "^1"
+
+"@types/d3-time-format@^2":
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-2.3.1.tgz#87a30e4513b9d1d53b920327a361f87255bf3372"
+  integrity sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==
+
+"@types/d3-time@^1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.1.1.tgz#6cf3a4242c3bbac00440dfb8ba7884f16bedfcbf"
+  integrity sha512-ULX7LoqXTCYtM+tLYOaeAJK7IwCT+4Gxlm2MaH0ErKLi07R5lh8NHCAyWcDkCCmx1AfRcBEV6H9QE9R25uP7jw==
+
+"@types/d3-timer@^1":
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-1.0.10.tgz#329c51c2c931f44ed0acff78b8c84571acf0ed21"
+  integrity sha512-ZnAbquVqy+4ZjdW0cY6URp+qF/AzTVNda2jYyOzpR2cPT35FTXl78s15Bomph9+ckOiI1TtkljnWkwbIGAb6rg==
+
+"@types/d3-transition@^1":
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.3.2.tgz#ed59beca7b4d679cfa52f88a6a50e5bbeb7e0a3c"
+  integrity sha512-J+a3SuF/E7wXbOSN19p8ZieQSFIm5hU2Egqtndbc54LXaAEOpLfDx4sBu/PKAKzHOdgKK1wkMhINKqNh4aoZAg==
+  dependencies:
+    "@types/d3-selection" "^1"
+
+"@types/d3-voronoi@*":
+  version "1.1.9"
+  resolved "https://registry.yarnpkg.com/@types/d3-voronoi/-/d3-voronoi-1.1.9.tgz#7bbc210818a3a5c5e0bafb051420df206617c9e5"
+  integrity sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ==
+
+"@types/d3-zoom@^1":
+  version "1.8.3"
+  resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-1.8.3.tgz#00237900c6fdc2bb4fe82679ee4d74eb8fbe7b3c"
+  integrity sha512-3kHkL6sPiDdbfGhzlp5gIHyu3kULhtnHTTAl3UBZVtWB1PzcLL8vdmz5mTx7plLiUqOA2Y+yT2GKjt/TdA2p7Q==
+  dependencies:
+    "@types/d3-interpolate" "^1"
+    "@types/d3-selection" "^1"
+
+"@types/d3@^5.7.2":
+  version "5.16.4"
+  resolved "https://registry.yarnpkg.com/@types/d3/-/d3-5.16.4.tgz#a7dc24a3dc1c19922eee72ba16144fd5bcea987a"
+  integrity sha512-2u0O9iP1MubFiQ+AhR1id4Egs+07BLtvRATG6IL2Gs9+KzdrfaxCKNq5hxEyw1kxwsqB/lCgr108XuHcKtb/5w==
+  dependencies:
+    "@types/d3-array" "^1"
+    "@types/d3-axis" "^1"
+    "@types/d3-brush" "^1"
+    "@types/d3-chord" "^1"
+    "@types/d3-collection" "*"
+    "@types/d3-color" "^1"
+    "@types/d3-contour" "^1"
+    "@types/d3-dispatch" "^1"
+    "@types/d3-drag" "^1"
+    "@types/d3-dsv" "^1"
+    "@types/d3-ease" "^1"
+    "@types/d3-fetch" "^1"
+    "@types/d3-force" "^1"
+    "@types/d3-format" "^1"
+    "@types/d3-geo" "^1"
+    "@types/d3-hierarchy" "^1"
+    "@types/d3-interpolate" "^1"
+    "@types/d3-path" "^1"
+    "@types/d3-polygon" "^1"
+    "@types/d3-quadtree" "^1"
+    "@types/d3-random" "^1"
+    "@types/d3-scale" "^2"
+    "@types/d3-scale-chromatic" "^1"
+    "@types/d3-selection" "^1"
+    "@types/d3-shape" "^1"
+    "@types/d3-time" "^1"
+    "@types/d3-time-format" "^2"
+    "@types/d3-timer" "^1"
+    "@types/d3-transition" "^1"
+    "@types/d3-voronoi" "*"
+    "@types/d3-zoom" "^1"
 
 "@types/eslint-scope@^3.7.0":
   version "3.7.0"
@@ -1370,6 +1928,11 @@
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.47.tgz#d7a51db20f0650efec24cd04994f523d93172ed4"
   integrity sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==
 
+"@types/geojson@*":
+  version "7946.0.8"
+  resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca"
+  integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==
+
 "@types/glob@^7.1.1":
   version "7.1.3"
   resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
@@ -1378,11 +1941,45 @@
     "@types/minimatch" "*"
     "@types/node" "*"
 
+"@types/graceful-fs@^4.1.2":
+  version "4.1.5"
+  resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15"
+  integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==
+  dependencies:
+    "@types/node" "*"
+
+"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
+  integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==
+
+"@types/istanbul-lib-report@*":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686"
+  integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==
+  dependencies:
+    "@types/istanbul-lib-coverage" "*"
+
+"@types/istanbul-reports@^3.0.0":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff"
+  integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==
+  dependencies:
+    "@types/istanbul-lib-report" "*"
+
 "@types/jasmine@~3.6.0":
   version "3.6.11"
   resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.6.11.tgz#4b1d77aa9dfc757407cb9e277216d8e83553f09d"
   integrity sha512-S6pvzQDvMZHrkBz2Mcn/8Du7cpr76PlRJBAoHnSDNbulULsH5dp0Gns+WRyNX5LHejz/ljxK4/vIHK/caHt6SQ==
 
+"@types/jest@^26.0.24":
+  version "26.0.24"
+  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.24.tgz#943d11976b16739185913a1936e0de0c4a7d595a"
+  integrity sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==
+  dependencies:
+    jest-diff "^26.0.0"
+    pretty-format "^26.0.0"
+
 "@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6":
   version "7.0.7"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad"
@@ -1393,7 +1990,7 @@
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21"
   integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==
 
-"@types/node@*", "@types/node@>=10.0.0":
+"@types/node@*":
   version "15.12.2"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.2.tgz#1f2b42c4be7156ff4a6f914b2fb03d05fa84e38d"
   integrity sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==
@@ -1408,11 +2005,21 @@
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
   integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
 
+"@types/prettier@^2.1.5":
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.3.2.tgz#fc8c2825e4ed2142473b4a81064e6e081463d1b3"
+  integrity sha512-eI5Yrz3Qv4KPUa/nSIAi0h+qX0XyewOliug5F2QAtuRg6Kjg6jfmxe1GIwoIRhZspD1A0RP8ANrPwvEXXtRFog==
+
 "@types/source-list-map@*":
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
   integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
 
+"@types/stack-utils@^2.0.0":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
+  integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
+
 "@types/webpack-sources@^0.1.5":
   version "0.1.8"
   resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.8.tgz#078d75410435993ec8a0a2855e88706f3f751f81"
@@ -1422,6 +2029,25 @@
     "@types/source-list-map" "*"
     source-map "^0.6.1"
 
+"@types/yargs-parser@*":
+  version "20.2.1"
+  resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129"
+  integrity sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==
+
+"@types/yargs@^15.0.0":
+  version "15.0.14"
+  resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06"
+  integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==
+  dependencies:
+    "@types/yargs-parser" "*"
+
+"@types/yargs@^16.0.0":
+  version "16.0.4"
+  resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977"
+  integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==
+  dependencies:
+    "@types/yargs-parser" "*"
+
 "@webassemblyjs/ast@1.11.0":
   version "1.11.0"
   resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.0.tgz#a5aa679efdc9e51707a4207139da57920555961f"
@@ -1558,7 +2184,7 @@
   resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
   integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
 
-abab@^2.0.5:
+abab@^2.0.3, abab@^2.0.5:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
   integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
@@ -1576,11 +2202,34 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
     mime-types "~2.1.24"
     negotiator "0.6.2"
 
+acorn-globals@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45"
+  integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==
+  dependencies:
+    acorn "^7.1.1"
+    acorn-walk "^7.1.1"
+
+acorn-walk@^7.1.1:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
+  integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
+
+acorn@^7.1.1:
+  version "7.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
+  integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
+
 acorn@^8.2.1:
   version "8.4.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.0.tgz#af53266e698d7cffa416714b503066a82221be60"
   integrity sha512-ULr0LDaEqQrMFGyQ3bhJkLsbtrQ8QibAseGZeaSUiT/6zb9IvIkomWHJIvgvwad+hinRAgsI51JcWk2yvwyL+w==
 
+acorn@^8.2.4:
+  version "8.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
+  integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==
+
 adjust-sourcemap-loader@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99"
@@ -1721,6 +2370,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   dependencies:
     color-convert "^2.0.1"
 
+ansi-styles@^5.0.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
+  integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
+
 anymatch@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
@@ -1729,7 +2383,7 @@ anymatch@^2.0.0:
     micromatch "^3.1.4"
     normalize-path "^2.1.1"
 
-anymatch@~3.1.2:
+anymatch@^3.0.3, anymatch@~3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
   integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
@@ -1750,6 +2404,13 @@ are-we-there-yet@~1.1.2:
     delegates "^1.0.0"
     readable-stream "^2.0.6"
 
+argparse@^1.0.7:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+  integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+  dependencies:
+    sprintf-js "~1.0.2"
+
 arr-diff@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@@ -1864,6 +2525,20 @@ aws4@^1.8.0:
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
   integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
 
+babel-jest@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.0.6.tgz#e99c6e0577da2655118e3608b68761a5a69bd0d8"
+  integrity sha512-iTJyYLNc4wRofASmofpOc5NK9QunwMk+TLFgGXsTFS8uEqmd8wdI7sga0FPe2oVH3b5Agt/EAK1QjPEuKL8VfA==
+  dependencies:
+    "@jest/transform" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    "@types/babel__core" "^7.1.14"
+    babel-plugin-istanbul "^6.0.0"
+    babel-preset-jest "^27.0.6"
+    chalk "^4.0.0"
+    graceful-fs "^4.2.4"
+    slash "^3.0.0"
+
 babel-loader@8.2.2:
   version "8.2.2"
   resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.2.tgz#9363ce84c10c9a40e6c753748e1441b60c8a0b81"
@@ -1881,6 +2556,27 @@ babel-plugin-dynamic-import-node@^2.3.3:
   dependencies:
     object.assign "^4.1.0"
 
+babel-plugin-istanbul@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz#e159ccdc9af95e0b570c75b4573b7c34d671d765"
+  integrity sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.0.0"
+    "@istanbuljs/load-nyc-config" "^1.0.0"
+    "@istanbuljs/schema" "^0.1.2"
+    istanbul-lib-instrument "^4.0.0"
+    test-exclude "^6.0.0"
+
+babel-plugin-jest-hoist@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.0.6.tgz#f7c6b3d764af21cb4a2a1ab6870117dbde15b456"
+  integrity sha512-CewFeM9Vv2gM7Yr9n5eyyLVPRSiBnk6lKZRjgwYnGKSl9M14TMn2vkN02wTF04OGuSDLEzlWiMzvjXuW9mB6Gw==
+  dependencies:
+    "@babel/template" "^7.3.3"
+    "@babel/types" "^7.3.3"
+    "@types/babel__core" "^7.0.0"
+    "@types/babel__traverse" "^7.0.6"
+
 babel-plugin-polyfill-corejs2@^0.2.0:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz#e9124785e6fd94f94b618a7954e5693053bf5327"
@@ -1905,16 +2601,37 @@ babel-plugin-polyfill-regenerator@^0.2.0:
   dependencies:
     "@babel/helper-define-polyfill-provider" "^0.2.2"
 
+babel-preset-current-node-syntax@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b"
+  integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==
+  dependencies:
+    "@babel/plugin-syntax-async-generators" "^7.8.4"
+    "@babel/plugin-syntax-bigint" "^7.8.3"
+    "@babel/plugin-syntax-class-properties" "^7.8.3"
+    "@babel/plugin-syntax-import-meta" "^7.8.3"
+    "@babel/plugin-syntax-json-strings" "^7.8.3"
+    "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+    "@babel/plugin-syntax-numeric-separator" "^7.8.3"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.3"
+    "@babel/plugin-syntax-top-level-await" "^7.8.3"
+
+babel-preset-jest@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.0.6.tgz#909ef08e9f24a4679768be2f60a3df0856843f9d"
+  integrity sha512-WObA0/Biw2LrVVwZkF/2GqbOdzhKD6Fkdwhoy9ASIrOWr/zodcSpQh72JOkEn6NWyjmnPDjNSqaGN4KnpKzhXw==
+  dependencies:
+    babel-plugin-jest-hoist "^27.0.6"
+    babel-preset-current-node-syntax "^1.0.0"
+
 balanced-match@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
-base64-arraybuffer@0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
-  integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=
-
 base64-js@1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
@@ -1925,11 +2642,6 @@ base64-js@^1.3.1:
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
   integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
 
-base64id@2.0.0, base64id@~2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
-  integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
-
 base@^0.11.1:
   version "0.11.2"
   resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
@@ -1986,7 +2698,7 @@ bl@^4.1.0:
     inherits "^2.0.4"
     readable-stream "^3.4.0"
 
-body-parser@1.19.0, body-parser@^1.19.0:
+body-parser@1.19.0:
   version "1.19.0"
   resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
   integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
@@ -2048,13 +2760,18 @@ braces@^2.3.1, braces@^2.3.2:
     split-string "^3.0.2"
     to-regex "^3.0.1"
 
-braces@^3.0.1, braces@^3.0.2, braces@~3.0.2:
+braces@^3.0.1, braces@~3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
   integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
   dependencies:
     fill-range "^7.0.1"
 
+browser-process-hrtime@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
+  integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
+
 browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.0, browserslist@^4.16.6, browserslist@^4.6.4, browserslist@^4.9.1:
   version "4.16.6"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2"
@@ -2066,7 +2783,21 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4
     escalade "^3.1.1"
     node-releases "^1.1.71"
 
-buffer-from@^1.0.0:
+bs-logger@0.x:
+  version "0.2.6"
+  resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
+  integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==
+  dependencies:
+    fast-json-stable-stringify "2.x"
+
+bser@2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05"
+  integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==
+  dependencies:
+    node-int64 "^0.4.0"
+
+buffer-from@1.x, buffer-from@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
   integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
@@ -2173,7 +2904,7 @@ callsites@^3.0.0:
   resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
   integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 
-camelcase@^5.0.0:
+camelcase@^5.0.0, camelcase@^5.3.1:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@@ -2217,7 +2948,7 @@ chalk@^2.0.0, chalk@^2.4.2:
     escape-string-regexp "^1.0.5"
     supports-color "^5.3.0"
 
-chalk@^4.1.0:
+chalk@^4.0.0, chalk@^4.1.0:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
   integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
@@ -2225,12 +2956,17 @@ chalk@^4.1.0:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
+char-regex@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
+  integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
+
 chardet@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
   integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
 
-"chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.5.1:
+"chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0:
   version "3.5.2"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
   integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
@@ -2274,11 +3010,21 @@ chrome-trace-event@^1.0.2:
   resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
   integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
 
+ci-info@^3.1.1:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.2.0.tgz#2876cb948a498797b5236f0095bc057d0dca38b6"
+  integrity sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==
+
 circular-dependency-plugin@5.2.2:
   version "5.2.2"
   resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz#39e836079db1d3cf2f988dc48c5188a44058b600"
   integrity sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ==
 
+cjs-module-lexer@^1.0.0:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40"
+  integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==
+
 class-utils@^0.3.5:
   version "0.3.6"
   resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
@@ -2343,11 +3089,21 @@ clone@^1.0.2:
   resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
   integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
 
+co@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+  integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+
 code-point-at@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
   integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
 
+collect-v8-coverage@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
+  integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==
+
 collection-visit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@@ -2390,19 +3146,14 @@ colorette@^1.2.1, colorette@^1.2.2:
   resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
   integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
 
-colors@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
-  integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
-
-combined-stream@^1.0.6, combined-stream@~1.0.6:
+combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
   integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
   dependencies:
     delayed-stream "~1.0.0"
 
-commander@^2.20.0:
+commander@2, commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -2417,7 +3168,7 @@ commondir@^1.0.1:
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
   integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
 
-component-emitter@^1.2.1, component-emitter@~1.3.0:
+component-emitter@^1.2.1:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
   integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
@@ -2452,16 +3203,6 @@ connect-history-api-fallback@^1.6.0:
   resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
   integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
 
-connect@^3.7.0:
-  version "3.7.0"
-  resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8"
-  integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==
-  dependencies:
-    debug "2.6.9"
-    finalhandler "1.1.2"
-    parseurl "~1.3.3"
-    utils-merge "1.0.1"
-
 console-control-strings@^1.0.0, console-control-strings@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
@@ -2479,6 +3220,13 @@ content-type@~1.0.4:
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
 
+convert-source-map@^1.4.0, convert-source-map@^1.6.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
+  integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
+  dependencies:
+    safe-buffer "~5.1.1"
+
 convert-source-map@^1.5.1, convert-source-map@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
@@ -2496,11 +3244,6 @@ cookie@0.4.0:
   resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
   integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
 
-cookie@~0.4.1:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
-  integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
-
 copy-anything@^2.0.1:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.3.tgz#842407ba02466b0df844819bbe3baebbe5d45d87"
@@ -2544,14 +3287,6 @@ core-util-is@1.0.2, core-util-is@~1.0.0:
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
   integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
 
-cors@~2.8.5:
-  version "2.8.5"
-  resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
-  integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
-  dependencies:
-    object-assign "^4"
-    vary "^1"
-
 cosmiconfig@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3"
@@ -2585,6 +3320,15 @@ cross-spawn@^6.0.0:
     shebang-command "^1.2.0"
     which "^1.2.9"
 
+cross-spawn@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+  dependencies:
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
+
 css-blank-pseudo@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5"
@@ -2775,10 +3519,270 @@ csso@^4.2.0:
   dependencies:
     css-tree "^1.1.2"
 
-custom-event@~1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
-  integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=
+cssom@^0.4.4:
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
+  integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
+
+cssom@~0.3.6:
+  version "0.3.8"
+  resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
+  integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
+
+cssstyle@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852"
+  integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
+  dependencies:
+    cssom "~0.3.6"
+
+d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
+  integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
+
+d3-axis@1:
+  version "1.0.12"
+  resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9"
+  integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==
+
+d3-brush@1:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.1.6.tgz#b0a22c7372cabec128bdddf9bddc058592f89e9b"
+  integrity sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==
+  dependencies:
+    d3-dispatch "1"
+    d3-drag "1"
+    d3-interpolate "1"
+    d3-selection "1"
+    d3-transition "1"
+
+d3-chord@1:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.6.tgz#309157e3f2db2c752f0280fedd35f2067ccbb15f"
+  integrity sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==
+  dependencies:
+    d3-array "1"
+    d3-path "1"
+
+d3-collection@1:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
+  integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
+
+d3-color@1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a"
+  integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==
+
+d3-contour@1:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3"
+  integrity sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==
+  dependencies:
+    d3-array "^1.1.1"
+
+d3-dispatch@1:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58"
+  integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==
+
+d3-drag@1:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.5.tgz#2537f451acd39d31406677b7dc77c82f7d988f70"
+  integrity sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==
+  dependencies:
+    d3-dispatch "1"
+    d3-selection "1"
+
+d3-dsv@1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.2.0.tgz#9d5f75c3a5f8abd611f74d3f5847b0d4338b885c"
+  integrity sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==
+  dependencies:
+    commander "2"
+    iconv-lite "0.4"
+    rw "1"
+
+d3-ease@1:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2"
+  integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==
+
+d3-fetch@1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.2.0.tgz#15ce2ecfc41b092b1db50abd2c552c2316cf7fc7"
+  integrity sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==
+  dependencies:
+    d3-dsv "1"
+
+d3-force@1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.2.1.tgz#fd29a5d1ff181c9e7f0669e4bd72bdb0e914ec0b"
+  integrity sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==
+  dependencies:
+    d3-collection "1"
+    d3-dispatch "1"
+    d3-quadtree "1"
+    d3-timer "1"
+
+d3-format@1:
+  version "1.4.5"
+  resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4"
+  integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==
+
+d3-geo@1:
+  version "1.12.1"
+  resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.12.1.tgz#7fc2ab7414b72e59fbcbd603e80d9adc029b035f"
+  integrity sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==
+  dependencies:
+    d3-array "1"
+
+d3-hierarchy@1:
+  version "1.1.9"
+  resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83"
+  integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==
+
+d3-interpolate@1:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987"
+  integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==
+  dependencies:
+    d3-color "1"
+
+d3-path@1:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
+  integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
+
+d3-polygon@1:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.6.tgz#0bf8cb8180a6dc107f518ddf7975e12abbfbd38e"
+  integrity sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==
+
+d3-quadtree@1:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz#ca8b84df7bb53763fe3c2f24bd435137f4e53135"
+  integrity sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==
+
+d3-random@1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.2.tgz#2833be7c124360bf9e2d3fd4f33847cfe6cab291"
+  integrity sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==
+
+d3-scale-chromatic@1:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98"
+  integrity sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==
+  dependencies:
+    d3-color "1"
+    d3-interpolate "1"
+
+d3-scale@2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f"
+  integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==
+  dependencies:
+    d3-array "^1.2.0"
+    d3-collection "1"
+    d3-format "1"
+    d3-interpolate "1"
+    d3-time "1"
+    d3-time-format "2"
+
+d3-selection@1, d3-selection@^1.1.0:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.2.tgz#dcaa49522c0dbf32d6c1858afc26b6094555bc5c"
+  integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==
+
+d3-shape@1:
+  version "1.3.7"
+  resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
+  integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==
+  dependencies:
+    d3-path "1"
+
+d3-time-format@2:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850"
+  integrity sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==
+  dependencies:
+    d3-time "1"
+
+d3-time@1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1"
+  integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==
+
+d3-timer@1:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5"
+  integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==
+
+d3-transition@1:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398"
+  integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==
+  dependencies:
+    d3-color "1"
+    d3-dispatch "1"
+    d3-ease "1"
+    d3-interpolate "1"
+    d3-selection "^1.1.0"
+    d3-timer "1"
+
+d3-voronoi@1:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297"
+  integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==
+
+d3-zoom@1:
+  version "1.8.3"
+  resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.8.3.tgz#b6a3dbe738c7763121cd05b8a7795ffe17f4fc0a"
+  integrity sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==
+  dependencies:
+    d3-dispatch "1"
+    d3-drag "1"
+    d3-interpolate "1"
+    d3-selection "1"
+    d3-transition "1"
+
+d3@^5.15.1:
+  version "5.16.0"
+  resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877"
+  integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==
+  dependencies:
+    d3-array "1"
+    d3-axis "1"
+    d3-brush "1"
+    d3-chord "1"
+    d3-collection "1"
+    d3-color "1"
+    d3-contour "1"
+    d3-dispatch "1"
+    d3-drag "1"
+    d3-dsv "1"
+    d3-ease "1"
+    d3-fetch "1"
+    d3-force "1"
+    d3-format "1"
+    d3-geo "1"
+    d3-hierarchy "1"
+    d3-interpolate "1"
+    d3-path "1"
+    d3-polygon "1"
+    d3-quadtree "1"
+    d3-random "1"
+    d3-scale "2"
+    d3-scale-chromatic "1"
+    d3-selection "1"
+    d3-shape "1"
+    d3-time "1"
+    d3-time-format "2"
+    d3-timer "1"
+    d3-transition "1"
+    d3-voronoi "1"
+    d3-zoom "1"
 
 dashdash@^1.12.0:
   version "1.14.1"
@@ -2787,15 +3791,14 @@ dashdash@^1.12.0:
   dependencies:
     assert-plus "^1.0.0"
 
-date-format@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf"
-  integrity sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==
-
-date-format@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/date-format/-/date-format-3.0.0.tgz#eb8780365c7d2b1511078fb491e6479780f3ad95"
-  integrity sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==
+data-urls@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
+  integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==
+  dependencies:
+    abab "^2.0.3"
+    whatwg-mimetype "^2.3.0"
+    whatwg-url "^8.0.0"
 
 debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
   version "2.6.9"
@@ -2804,7 +3807,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
   dependencies:
     ms "2.0.0"
 
-debug@4, debug@4.3.1, debug@^4.1.0, debug@^4.1.1, debug@~4.3.1:
+debug@4, debug@4.3.1, debug@^4.1.0, debug@^4.1.1:
   version "4.3.1"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
   integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
@@ -2830,11 +3833,21 @@ decamelize@^1.2.0:
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
   integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
 
+decimal.js@^10.2.1:
+  version "10.3.1"
+  resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
+  integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==
+
 decode-uri-component@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
   integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
 
+dedent@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
+  integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
+
 deep-equal@^1.0.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
@@ -2847,6 +3860,16 @@ deep-equal@^1.0.1:
     object-keys "^1.1.1"
     regexp.prototype.flags "^1.2.0"
 
+deep-is@~0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+  integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+
+deepmerge@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
+  integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
+
 default-gateway@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b"
@@ -2934,15 +3957,25 @@ destroy@~1.0.4:
   resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
   integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
 
+detect-newline@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
+  integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
+
 detect-node@^2.0.4:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
   integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
 
-di@^0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
-  integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=
+diff-sequences@^26.6.2:
+  version "26.6.2"
+  resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
+  integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==
+
+diff-sequences@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723"
+  integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==
 
 dir-glob@^3.0.1:
   version "3.0.1"
@@ -2971,16 +4004,6 @@ dns-txt@^2.0.2:
   dependencies:
     buffer-indexof "^1.0.0"
 
-dom-serialize@^2.2.1:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
-  integrity sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=
-  dependencies:
-    custom-event "~1.0.0"
-    ent "~2.2.0"
-    extend "^3.0.0"
-    void-elements "^2.0.0"
-
 dom-serializer@^1.0.1:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91"
@@ -2995,6 +4018,13 @@ domelementtype@^2.0.1, domelementtype@^2.2.0:
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
   integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
 
+domexception@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304"
+  integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==
+  dependencies:
+    webidl-conversions "^5.0.0"
+
 domhandler@^4.0.0, domhandler@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059"
@@ -3029,6 +4059,11 @@ electron-to-chromium@^1.3.723:
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz#0728587f1b9b970ec9ffad932496429aef750d09"
   integrity sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A==
 
+emittery@^0.8.1:
+  version "0.8.1"
+  resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860"
+  integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==
+
 emoji-regex@^7.0.1:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
@@ -3063,26 +4098,6 @@ end-of-stream@^1.1.0:
   dependencies:
     once "^1.4.0"
 
-engine.io-parser@~4.0.0:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.2.tgz#e41d0b3fb66f7bf4a3671d2038a154024edb501e"
-  integrity sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==
-  dependencies:
-    base64-arraybuffer "0.1.4"
-
-engine.io@~4.1.0:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-4.1.1.tgz#9a8f8a5ac5a5ea316183c489bf7f5b6cf91ace5b"
-  integrity sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w==
-  dependencies:
-    accepts "~1.3.4"
-    base64id "2.0.0"
-    cookie "~0.4.1"
-    cors "~2.8.5"
-    debug "~4.3.1"
-    engine.io-parser "~4.0.0"
-    ws "~7.4.2"
-
 enhanced-resolve@5.7.0:
   version "5.7.0"
   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz#525c5d856680fbd5052de453ac83e32049958b5c"
@@ -3099,11 +4114,6 @@ enhanced-resolve@^5.8.0:
     graceful-fs "^4.2.4"
     tapable "^2.2.0"
 
-ent@~2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
-  integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
-
 entities@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
@@ -3153,6 +4163,23 @@ escape-string-regexp@^1.0.5:
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
+escape-string-regexp@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
+  integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
+
+escodegen@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd"
+  integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==
+  dependencies:
+    esprima "^4.0.1"
+    estraverse "^5.2.0"
+    esutils "^2.0.2"
+    optionator "^0.8.1"
+  optionalDependencies:
+    source-map "~0.6.1"
+
 eslint-scope@5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
@@ -3161,6 +4188,11 @@ eslint-scope@5.1.1:
     esrecurse "^4.3.0"
     estraverse "^4.1.1"
 
+esprima@^4.0.0, esprima@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+  integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
 esrecurse@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
@@ -3218,6 +4250,26 @@ execa@^1.0.0:
     signal-exit "^3.0.0"
     strip-eof "^1.0.0"
 
+execa@^5.0.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
+  integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
+  dependencies:
+    cross-spawn "^7.0.3"
+    get-stream "^6.0.0"
+    human-signals "^2.1.0"
+    is-stream "^2.0.0"
+    merge-stream "^2.0.0"
+    npm-run-path "^4.0.1"
+    onetime "^5.1.2"
+    signal-exit "^3.0.3"
+    strip-final-newline "^2.0.0"
+
+exit@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
+  integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=
+
 expand-brackets@^2.1.4:
   version "2.1.4"
   resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
@@ -3231,6 +4283,18 @@ expand-brackets@^2.1.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
+expect@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/expect/-/expect-27.0.6.tgz#a4d74fbe27222c718fff68ef49d78e26a8fd4c05"
+  integrity sha512-psNLt8j2kwg42jGBDSfAlU49CEZxejN1f1PlANWDZqIhBOVU/c2Pm888FcjWJzFewhIsNWfZJeLjUjtKGiPuSw==
+  dependencies:
+    "@jest/types" "^27.0.6"
+    ansi-styles "^5.0.0"
+    jest-get-type "^27.0.6"
+    jest-matcher-utils "^27.0.6"
+    jest-message-util "^27.0.6"
+    jest-regex-util "^27.0.6"
+
 express@^4.17.1:
   version "4.17.1"
   resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@@ -3282,7 +4346,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
     assign-symbols "^1.0.0"
     is-extendable "^1.0.1"
 
-extend@^3.0.0, extend@~3.0.2:
+extend@~3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
   integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
@@ -3337,11 +4401,16 @@ fast-glob@^3.1.1, fast-glob@^3.2.5:
     micromatch "^4.0.2"
     picomatch "^2.2.1"
 
-fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@^2.0.0:
+fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
 
+fast-levenshtein@~2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+  integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+
 fastq@^1.6.0:
   version "1.11.0"
   resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858"
@@ -3356,6 +4425,13 @@ faye-websocket@^0.11.3:
   dependencies:
     websocket-driver ">=0.5.1"
 
+fb-watchman@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85"
+  integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==
+  dependencies:
+    bser "2.1.1"
+
 figures@^3.0.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
@@ -3385,7 +4461,7 @@ fill-range@^7.0.1:
   dependencies:
     to-regex-range "^5.0.1"
 
-finalhandler@1.1.2, finalhandler@~1.1.2:
+finalhandler@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
   integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
@@ -3414,7 +4490,7 @@ find-up@^3.0.0:
   dependencies:
     locate-path "^3.0.0"
 
-find-up@^4.0.0:
+find-up@^4.0.0, find-up@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
   integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
@@ -3422,11 +4498,6 @@ find-up@^4.0.0:
     locate-path "^5.0.0"
     path-exists "^4.0.0"
 
-flatted@^2.0.1:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
-  integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
-
 flatten@^1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b"
@@ -3447,6 +4518,15 @@ forever-agent@~0.6.1:
   resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
   integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
 
+form-data@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
+  integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
 form-data@~2.3.2:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
@@ -3473,15 +4553,6 @@ fresh@0.5.2:
   resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
   integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
 
-fs-extra@^8.1.0:
-  version "8.1.0"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
-  integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
-  dependencies:
-    graceful-fs "^4.2.0"
-    jsonfile "^4.0.0"
-    universalify "^0.1.0"
-
 fs-minipass@^2.0.0, fs-minipass@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
@@ -3507,7 +4578,7 @@ fsevents@^1.2.7:
     bindings "^1.5.0"
     nan "^2.12.1"
 
-fsevents@~2.3.2:
+fsevents@^2.3.2, fsevents@~2.3.2:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
   integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
@@ -3550,6 +4621,11 @@ get-intrinsic@^1.0.2:
     has "^1.0.3"
     has-symbols "^1.0.1"
 
+get-package-type@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
+  integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
+
 get-stream@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
@@ -3557,6 +4633,11 @@ get-stream@^4.0.0:
   dependencies:
     pump "^3.0.0"
 
+get-stream@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
+  integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
+
 get-value@^2.0.3, get-value@^2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
@@ -3589,7 +4670,7 @@ glob-to-regexp@^0.4.1:
   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
   integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
 
-glob@7.1.7, glob@^7.0.3, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7:
+glob@7.1.7, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
   version "7.1.7"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
   integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
@@ -3629,7 +4710,7 @@ globby@^6.1.0:
     pify "^2.0.0"
     pinkie-promise "^2.0.0"
 
-graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.3, graceful-fs@^4.2.4, graceful-fs@^4.2.6:
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4:
   version "4.2.6"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
   integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
@@ -3742,6 +4823,13 @@ hsla-regex@^1.0.0:
   resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
   integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
 
+html-encoding-sniffer@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
+  integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==
+  dependencies:
+    whatwg-encoding "^1.0.5"
+
 html-entities@^1.3.1:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc"
@@ -3818,7 +4906,7 @@ http-proxy-middleware@0.19.1:
     lodash "^4.17.11"
     micromatch "^3.1.10"
 
-http-proxy@^1.17.0, http-proxy@^1.18.1:
+http-proxy@^1.17.0:
   version "1.18.1"
   resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
   integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
@@ -3844,6 +4932,11 @@ https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0:
     agent-base "6"
     debug "4"
 
+human-signals@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
+  integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
+
 humanize-ms@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
@@ -3851,7 +4944,7 @@ humanize-ms@^1.2.1:
   dependencies:
     ms "^2.0.0"
 
-iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
+iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
   integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -3908,6 +5001,14 @@ import-local@^2.0.0:
     pkg-dir "^3.0.0"
     resolve-cwd "^2.0.0"
 
+import-local@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6"
+  integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==
+  dependencies:
+    pkg-dir "^4.2.0"
+    resolve-cwd "^3.0.0"
+
 imurmurhash@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
@@ -4043,6 +5144,13 @@ is-buffer@^1.1.5:
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
+is-ci@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.0.tgz#c7e7be3c9d8eef7d0fa144390bd1e4b88dc4c994"
+  integrity sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==
+  dependencies:
+    ci-info "^3.1.1"
+
 is-color-stop@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345"
@@ -4138,6 +5246,11 @@ is-fullwidth-code-point@^3.0.0:
   resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
   integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
 
+is-generator-fn@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
+  integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
+
 is-glob@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
@@ -4200,6 +5313,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
   dependencies:
     isobject "^3.0.1"
 
+is-potential-custom-element-name@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
+  integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
+
 is-regex@^1.0.4:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
@@ -4218,7 +5336,12 @@ is-stream@^1.1.0:
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
   integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
 
-is-typedarray@~1.0.0:
+is-stream@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
+  integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
+
+is-typedarray@^1.0.0, is-typedarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
   integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
@@ -4255,11 +5378,6 @@ isarray@1.0.0, isarray@~1.0.0:
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
   integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
 
-isbinaryfile@^4.0.8:
-  version "4.0.8"
-  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.8.tgz#5d34b94865bd4946633ecc78a026fc76c5b11fcf"
-  integrity sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==
-
 isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -4287,7 +5405,7 @@ istanbul-lib-coverage@^3.0.0:
   resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec"
   integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==
 
-istanbul-lib-instrument@^4.0.1, istanbul-lib-instrument@^4.0.3:
+istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.3:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d"
   integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==
@@ -4306,27 +5424,442 @@ istanbul-lib-report@^3.0.0:
     make-dir "^3.0.0"
     supports-color "^7.1.0"
 
-istanbul-lib-source-maps@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9"
-  integrity sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==
+istanbul-lib-source-maps@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9"
+  integrity sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==
+  dependencies:
+    debug "^4.1.1"
+    istanbul-lib-coverage "^3.0.0"
+    source-map "^0.6.1"
+
+istanbul-reports@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b"
+  integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==
+  dependencies:
+    html-escaper "^2.0.0"
+    istanbul-lib-report "^3.0.0"
+
+jasmine-core@~3.7.0:
+  version "3.7.1"
+  resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.7.1.tgz#0401327f6249eac993d47bbfa18d4e8efacfb561"
+  integrity sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ==
+
+jest-changed-files@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.0.6.tgz#bed6183fcdea8a285482e3b50a9a7712d49a7a8b"
+  integrity sha512-BuL/ZDauaq5dumYh5y20sn4IISnf1P9A0TDswTxUi84ORGtVa86ApuBHqICL0vepqAnZiY6a7xeSPWv2/yy4eA==
+  dependencies:
+    "@jest/types" "^27.0.6"
+    execa "^5.0.0"
+    throat "^6.0.1"
+
+jest-circus@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.0.6.tgz#dd4df17c4697db6a2c232aaad4e9cec666926668"
+  integrity sha512-OJlsz6BBeX9qR+7O9lXefWoc2m9ZqcZ5Ohlzz0pTEAG4xMiZUJoacY8f4YDHxgk0oKYxj277AfOk9w6hZYvi1Q==
+  dependencies:
+    "@jest/environment" "^27.0.6"
+    "@jest/test-result" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    "@types/node" "*"
+    chalk "^4.0.0"
+    co "^4.6.0"
+    dedent "^0.7.0"
+    expect "^27.0.6"
+    is-generator-fn "^2.0.0"
+    jest-each "^27.0.6"
+    jest-matcher-utils "^27.0.6"
+    jest-message-util "^27.0.6"
+    jest-runtime "^27.0.6"
+    jest-snapshot "^27.0.6"
+    jest-util "^27.0.6"
+    pretty-format "^27.0.6"
+    slash "^3.0.0"
+    stack-utils "^2.0.3"
+    throat "^6.0.1"
+
+jest-cli@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.0.6.tgz#d021e5f4d86d6a212450d4c7b86cb219f1e6864f"
+  integrity sha512-qUUVlGb9fdKir3RDE+B10ULI+LQrz+MCflEH2UJyoUjoHHCbxDrMxSzjQAPUMsic4SncI62ofYCcAvW6+6rhhg==
+  dependencies:
+    "@jest/core" "^27.0.6"
+    "@jest/test-result" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    chalk "^4.0.0"
+    exit "^0.1.2"
+    graceful-fs "^4.2.4"
+    import-local "^3.0.2"
+    jest-config "^27.0.6"
+    jest-util "^27.0.6"
+    jest-validate "^27.0.6"
+    prompts "^2.0.1"
+    yargs "^16.0.3"
+
+jest-config@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.0.6.tgz#119fb10f149ba63d9c50621baa4f1f179500277f"
+  integrity sha512-JZRR3I1Plr2YxPBhgqRspDE2S5zprbga3swYNrvY3HfQGu7p/GjyLOqwrYad97tX3U3mzT53TPHVmozacfP/3w==
+  dependencies:
+    "@babel/core" "^7.1.0"
+    "@jest/test-sequencer" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    babel-jest "^27.0.6"
+    chalk "^4.0.0"
+    deepmerge "^4.2.2"
+    glob "^7.1.1"
+    graceful-fs "^4.2.4"
+    is-ci "^3.0.0"
+    jest-circus "^27.0.6"
+    jest-environment-jsdom "^27.0.6"
+    jest-environment-node "^27.0.6"
+    jest-get-type "^27.0.6"
+    jest-jasmine2 "^27.0.6"
+    jest-regex-util "^27.0.6"
+    jest-resolve "^27.0.6"
+    jest-runner "^27.0.6"
+    jest-util "^27.0.6"
+    jest-validate "^27.0.6"
+    micromatch "^4.0.4"
+    pretty-format "^27.0.6"
+
+jest-diff@^26.0.0:
+  version "26.6.2"
+  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394"
+  integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==
+  dependencies:
+    chalk "^4.0.0"
+    diff-sequences "^26.6.2"
+    jest-get-type "^26.3.0"
+    pretty-format "^26.6.2"
+
+jest-diff@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.0.6.tgz#4a7a19ee6f04ad70e0e3388f35829394a44c7b5e"
+  integrity sha512-Z1mqgkTCSYaFgwTlP/NUiRzdqgxmmhzHY1Tq17zL94morOHfHu3K4bgSgl+CR4GLhpV8VxkuOYuIWnQ9LnFqmg==
+  dependencies:
+    chalk "^4.0.0"
+    diff-sequences "^27.0.6"
+    jest-get-type "^27.0.6"
+    pretty-format "^27.0.6"
+
+jest-docblock@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.0.6.tgz#cc78266acf7fe693ca462cbbda0ea4e639e4e5f3"
+  integrity sha512-Fid6dPcjwepTFraz0YxIMCi7dejjJ/KL9FBjPYhBp4Sv1Y9PdhImlKZqYU555BlN4TQKaTc+F2Av1z+anVyGkA==
+  dependencies:
+    detect-newline "^3.0.0"
+
+jest-each@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.0.6.tgz#cee117071b04060158dc8d9a66dc50ad40ef453b"
+  integrity sha512-m6yKcV3bkSWrUIjxkE9OC0mhBZZdhovIW5ergBYirqnkLXkyEn3oUUF/QZgyecA1cF1QFyTE8bRRl8Tfg1pfLA==
+  dependencies:
+    "@jest/types" "^27.0.6"
+    chalk "^4.0.0"
+    jest-get-type "^27.0.6"
+    jest-util "^27.0.6"
+    pretty-format "^27.0.6"
+
+jest-environment-jsdom@^27.0.0, jest-environment-jsdom@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.0.6.tgz#f66426c4c9950807d0a9f209c590ce544f73291f"
+  integrity sha512-FvetXg7lnXL9+78H+xUAsra3IeZRTiegA3An01cWeXBspKXUhAwMM9ycIJ4yBaR0L7HkoMPaZsozCLHh4T8fuw==
+  dependencies:
+    "@jest/environment" "^27.0.6"
+    "@jest/fake-timers" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    "@types/node" "*"
+    jest-mock "^27.0.6"
+    jest-util "^27.0.6"
+    jsdom "^16.6.0"
+
+jest-environment-node@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.0.6.tgz#a6699b7ceb52e8d68138b9808b0c404e505f3e07"
+  integrity sha512-+Vi6yLrPg/qC81jfXx3IBlVnDTI6kmRr08iVa2hFCWmJt4zha0XW7ucQltCAPhSR0FEKEoJ3i+W4E6T0s9is0w==
+  dependencies:
+    "@jest/environment" "^27.0.6"
+    "@jest/fake-timers" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    "@types/node" "*"
+    jest-mock "^27.0.6"
+    jest-util "^27.0.6"
+
+jest-get-type@^26.3.0:
+  version "26.3.0"
+  resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
+  integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==
+
+jest-get-type@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe"
+  integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==
+
+jest-haste-map@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.0.6.tgz#4683a4e68f6ecaa74231679dca237279562c8dc7"
+  integrity sha512-4ldjPXX9h8doB2JlRzg9oAZ2p6/GpQUNAeiYXqcpmrKbP0Qev0wdZlxSMOmz8mPOEnt4h6qIzXFLDi8RScX/1w==
+  dependencies:
+    "@jest/types" "^27.0.6"
+    "@types/graceful-fs" "^4.1.2"
+    "@types/node" "*"
+    anymatch "^3.0.3"
+    fb-watchman "^2.0.0"
+    graceful-fs "^4.2.4"
+    jest-regex-util "^27.0.6"
+    jest-serializer "^27.0.6"
+    jest-util "^27.0.6"
+    jest-worker "^27.0.6"
+    micromatch "^4.0.4"
+    walker "^1.0.7"
+  optionalDependencies:
+    fsevents "^2.3.2"
+
+jest-jasmine2@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.0.6.tgz#fd509a9ed3d92bd6edb68a779f4738b100655b37"
+  integrity sha512-cjpH2sBy+t6dvCeKBsHpW41mjHzXgsavaFMp+VWRf0eR4EW8xASk1acqmljFtK2DgyIECMv2yCdY41r2l1+4iA==
+  dependencies:
+    "@babel/traverse" "^7.1.0"
+    "@jest/environment" "^27.0.6"
+    "@jest/source-map" "^27.0.6"
+    "@jest/test-result" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    "@types/node" "*"
+    chalk "^4.0.0"
+    co "^4.6.0"
+    expect "^27.0.6"
+    is-generator-fn "^2.0.0"
+    jest-each "^27.0.6"
+    jest-matcher-utils "^27.0.6"
+    jest-message-util "^27.0.6"
+    jest-runtime "^27.0.6"
+    jest-snapshot "^27.0.6"
+    jest-util "^27.0.6"
+    pretty-format "^27.0.6"
+    throat "^6.0.1"
+
+jest-leak-detector@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.0.6.tgz#545854275f85450d4ef4b8fe305ca2a26450450f"
+  integrity sha512-2/d6n2wlH5zEcdctX4zdbgX8oM61tb67PQt4Xh8JFAIy6LRKUnX528HulkaG6nD5qDl5vRV1NXejCe1XRCH5gQ==
+  dependencies:
+    jest-get-type "^27.0.6"
+    pretty-format "^27.0.6"
+
+jest-matcher-utils@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.0.6.tgz#2a8da1e86c620b39459f4352eaa255f0d43e39a9"
+  integrity sha512-OFgF2VCQx9vdPSYTHWJ9MzFCehs20TsyFi6bIHbk5V1u52zJOnvF0Y/65z3GLZHKRuTgVPY4Z6LVePNahaQ+tA==
+  dependencies:
+    chalk "^4.0.0"
+    jest-diff "^27.0.6"
+    jest-get-type "^27.0.6"
+    pretty-format "^27.0.6"
+
+jest-message-util@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.0.6.tgz#158bcdf4785706492d164a39abca6a14da5ab8b5"
+  integrity sha512-rBxIs2XK7rGy+zGxgi+UJKP6WqQ+KrBbD1YMj517HYN3v2BG66t3Xan3FWqYHKZwjdB700KiAJ+iES9a0M+ixw==
+  dependencies:
+    "@babel/code-frame" "^7.12.13"
+    "@jest/types" "^27.0.6"
+    "@types/stack-utils" "^2.0.0"
+    chalk "^4.0.0"
+    graceful-fs "^4.2.4"
+    micromatch "^4.0.4"
+    pretty-format "^27.0.6"
+    slash "^3.0.0"
+    stack-utils "^2.0.3"
+
+jest-mock@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.0.6.tgz#0efdd40851398307ba16778728f6d34d583e3467"
+  integrity sha512-lzBETUoK8cSxts2NYXSBWT+EJNzmUVtVVwS1sU9GwE1DLCfGsngg+ZVSIe0yd0ZSm+y791esiuo+WSwpXJQ5Bw==
+  dependencies:
+    "@jest/types" "^27.0.6"
+    "@types/node" "*"
+
+jest-pnp-resolver@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c"
+  integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==
+
+jest-preset-angular@^9.0.5:
+  version "9.0.5"
+  resolved "https://registry.yarnpkg.com/jest-preset-angular/-/jest-preset-angular-9.0.5.tgz#0cbd86aee68a7ed3a84ffade7f40c277ce38b61c"
+  integrity sha512-FTvyTA1lXg6voqOV7gH/6ZXqalNCpD3Yuv+uJg11BAhO13wdjy/ebutiuQAqSq/Zu8zFS6oDIhCzan1EpyY0tQ==
+  dependencies:
+    jest-environment-jsdom "^27.0.0"
+    pretty-format "^27.0.0"
+    ts-jest "^27.0.0"
+
+jest-regex-util@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.0.6.tgz#02e112082935ae949ce5d13b2675db3d8c87d9c5"
+  integrity sha512-SUhPzBsGa1IKm8hx2F4NfTGGp+r7BXJ4CulsZ1k2kI+mGLG+lxGrs76veN2LF/aUdGosJBzKgXmNCw+BzFqBDQ==
+
+jest-resolve-dependencies@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.0.6.tgz#3e619e0ef391c3ecfcf6ef4056207a3d2be3269f"
+  integrity sha512-mg9x9DS3BPAREWKCAoyg3QucCr0n6S8HEEsqRCKSPjPcu9HzRILzhdzY3imsLoZWeosEbJZz6TKasveczzpJZA==
+  dependencies:
+    "@jest/types" "^27.0.6"
+    jest-regex-util "^27.0.6"
+    jest-snapshot "^27.0.6"
+
+jest-resolve@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.0.6.tgz#e90f436dd4f8fbf53f58a91c42344864f8e55bff"
+  integrity sha512-yKmIgw2LgTh7uAJtzv8UFHGF7Dm7XfvOe/LQ3Txv101fLM8cx2h1QVwtSJ51Q/SCxpIiKfVn6G2jYYMDNHZteA==
+  dependencies:
+    "@jest/types" "^27.0.6"
+    chalk "^4.0.0"
+    escalade "^3.1.1"
+    graceful-fs "^4.2.4"
+    jest-pnp-resolver "^1.2.2"
+    jest-util "^27.0.6"
+    jest-validate "^27.0.6"
+    resolve "^1.20.0"
+    slash "^3.0.0"
+
+jest-runner@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.0.6.tgz#1325f45055539222bbc7256a6976e993ad2f9520"
+  integrity sha512-W3Bz5qAgaSChuivLn+nKOgjqNxM7O/9JOJoKDCqThPIg2sH/d4A/lzyiaFgnb9V1/w29Le11NpzTJSzga1vyYQ==
+  dependencies:
+    "@jest/console" "^27.0.6"
+    "@jest/environment" "^27.0.6"
+    "@jest/test-result" "^27.0.6"
+    "@jest/transform" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    "@types/node" "*"
+    chalk "^4.0.0"
+    emittery "^0.8.1"
+    exit "^0.1.2"
+    graceful-fs "^4.2.4"
+    jest-docblock "^27.0.6"
+    jest-environment-jsdom "^27.0.6"
+    jest-environment-node "^27.0.6"
+    jest-haste-map "^27.0.6"
+    jest-leak-detector "^27.0.6"
+    jest-message-util "^27.0.6"
+    jest-resolve "^27.0.6"
+    jest-runtime "^27.0.6"
+    jest-util "^27.0.6"
+    jest-worker "^27.0.6"
+    source-map-support "^0.5.6"
+    throat "^6.0.1"
+
+jest-runtime@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.0.6.tgz#45877cfcd386afdd4f317def551fc369794c27c9"
+  integrity sha512-BhvHLRVfKibYyqqEFkybsznKwhrsu7AWx2F3y9G9L95VSIN3/ZZ9vBpm/XCS2bS+BWz3sSeNGLzI3TVQ0uL85Q==
+  dependencies:
+    "@jest/console" "^27.0.6"
+    "@jest/environment" "^27.0.6"
+    "@jest/fake-timers" "^27.0.6"
+    "@jest/globals" "^27.0.6"
+    "@jest/source-map" "^27.0.6"
+    "@jest/test-result" "^27.0.6"
+    "@jest/transform" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    "@types/yargs" "^16.0.0"
+    chalk "^4.0.0"
+    cjs-module-lexer "^1.0.0"
+    collect-v8-coverage "^1.0.0"
+    exit "^0.1.2"
+    glob "^7.1.3"
+    graceful-fs "^4.2.4"
+    jest-haste-map "^27.0.6"
+    jest-message-util "^27.0.6"
+    jest-mock "^27.0.6"
+    jest-regex-util "^27.0.6"
+    jest-resolve "^27.0.6"
+    jest-snapshot "^27.0.6"
+    jest-util "^27.0.6"
+    jest-validate "^27.0.6"
+    slash "^3.0.0"
+    strip-bom "^4.0.0"
+    yargs "^16.0.3"
+
+jest-serializer@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.0.6.tgz#93a6c74e0132b81a2d54623251c46c498bb5bec1"
+  integrity sha512-PtGdVK9EGC7dsaziskfqaAPib6wTViY3G8E5wz9tLVPhHyiDNTZn/xjZ4khAw+09QkoOVpn7vF5nPSN6dtBexA==
   dependencies:
-    debug "^4.1.1"
-    istanbul-lib-coverage "^3.0.0"
-    source-map "^0.6.1"
+    "@types/node" "*"
+    graceful-fs "^4.2.4"
 
-istanbul-reports@^3.0.0:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b"
-  integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==
+jest-snapshot@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.0.6.tgz#f4e6b208bd2e92e888344d78f0f650bcff05a4bf"
+  integrity sha512-NTHaz8He+ATUagUgE7C/UtFcRoHqR2Gc+KDfhQIyx+VFgwbeEMjeP+ILpUTLosZn/ZtbNdCF5LkVnN/l+V751A==
+  dependencies:
+    "@babel/core" "^7.7.2"
+    "@babel/generator" "^7.7.2"
+    "@babel/parser" "^7.7.2"
+    "@babel/plugin-syntax-typescript" "^7.7.2"
+    "@babel/traverse" "^7.7.2"
+    "@babel/types" "^7.0.0"
+    "@jest/transform" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    "@types/babel__traverse" "^7.0.4"
+    "@types/prettier" "^2.1.5"
+    babel-preset-current-node-syntax "^1.0.0"
+    chalk "^4.0.0"
+    expect "^27.0.6"
+    graceful-fs "^4.2.4"
+    jest-diff "^27.0.6"
+    jest-get-type "^27.0.6"
+    jest-haste-map "^27.0.6"
+    jest-matcher-utils "^27.0.6"
+    jest-message-util "^27.0.6"
+    jest-resolve "^27.0.6"
+    jest-util "^27.0.6"
+    natural-compare "^1.4.0"
+    pretty-format "^27.0.6"
+    semver "^7.3.2"
+
+jest-util@^27.0.0, jest-util@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.0.6.tgz#e8e04eec159de2f4d5f57f795df9cdc091e50297"
+  integrity sha512-1JjlaIh+C65H/F7D11GNkGDDZtDfMEM8EBXsvd+l/cxtgQ6QhxuloOaiayt89DxUvDarbVhqI98HhgrM1yliFQ==
   dependencies:
-    html-escaper "^2.0.0"
-    istanbul-lib-report "^3.0.0"
+    "@jest/types" "^27.0.6"
+    "@types/node" "*"
+    chalk "^4.0.0"
+    graceful-fs "^4.2.4"
+    is-ci "^3.0.0"
+    picomatch "^2.2.3"
 
-jasmine-core@^3.6.0, jasmine-core@~3.7.0:
-  version "3.7.1"
-  resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.7.1.tgz#0401327f6249eac993d47bbfa18d4e8efacfb561"
-  integrity sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ==
+jest-validate@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.0.6.tgz#930a527c7a951927df269f43b2dc23262457e2a6"
+  integrity sha512-yhZZOaMH3Zg6DC83n60pLmdU1DQE46DW+KLozPiPbSbPhlXXaiUTDlhHQhHFpaqIFRrInko1FHXjTRpjWRuWfA==
+  dependencies:
+    "@jest/types" "^27.0.6"
+    camelcase "^6.2.0"
+    chalk "^4.0.0"
+    jest-get-type "^27.0.6"
+    leven "^3.1.0"
+    pretty-format "^27.0.6"
+
+jest-watcher@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.0.6.tgz#89526f7f9edf1eac4e4be989bcb6dec6b8878d9c"
+  integrity sha512-/jIoKBhAP00/iMGnTwUBLgvxkn7vsOweDrOTSPzc7X9uOyUtJIDthQBTI1EXz90bdkrxorUZVhJwiB69gcHtYQ==
+  dependencies:
+    "@jest/test-result" "^27.0.6"
+    "@jest/types" "^27.0.6"
+    "@types/node" "*"
+    ansi-escapes "^4.2.1"
+    chalk "^4.0.0"
+    jest-util "^27.0.6"
+    string-length "^4.0.1"
 
 jest-worker@26.6.2, jest-worker@^26.3.0, jest-worker@^26.6.2:
   version "26.6.2"
@@ -4346,6 +5879,24 @@ jest-worker@^27.0.2:
     merge-stream "^2.0.0"
     supports-color "^8.0.0"
 
+jest-worker@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.6.tgz#a5fdb1e14ad34eb228cfe162d9f729cdbfa28aed"
+  integrity sha512-qupxcj/dRuA3xHPMUd40gr2EaAurFbkwzOh7wfPaeE9id7hyjURRQoqNfHifHK3XjJU6YJJUQKILGUnwGPEOCA==
+  dependencies:
+    "@types/node" "*"
+    merge-stream "^2.0.0"
+    supports-color "^8.0.0"
+
+jest@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest/-/jest-27.0.6.tgz#10517b2a628f0409087fbf473db44777d7a04505"
+  integrity sha512-EjV8aETrsD0wHl7CKMibKwQNQc3gIRBXlTikBmmHUeVMKaPFxdcUIBfoDqTSXDoGJIivAYGqCWVlzCSaVjPQsA==
+  dependencies:
+    "@jest/core" "^27.0.6"
+    import-local "^3.0.2"
+    jest-cli "^27.0.6"
+
 js-sha256@0.9.0:
   version "0.9.0"
   resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
@@ -4356,11 +5907,52 @@ js-tokens@^4.0.0:
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
   integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
 
+js-yaml@^3.13.1:
+  version "3.14.1"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
+  integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
+  dependencies:
+    argparse "^1.0.7"
+    esprima "^4.0.0"
+
 jsbn@~0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
   integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
 
+jsdom@^16.6.0:
+  version "16.6.0"
+  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.6.0.tgz#f79b3786682065492a3da6a60a4695da983805ac"
+  integrity sha512-Ty1vmF4NHJkolaEmdjtxTfSfkdb8Ywarwf63f+F8/mDD1uLSSWDxDuMiZxiPhwunLrn9LOSVItWj4bLYsLN3Dg==
+  dependencies:
+    abab "^2.0.5"
+    acorn "^8.2.4"
+    acorn-globals "^6.0.0"
+    cssom "^0.4.4"
+    cssstyle "^2.3.0"
+    data-urls "^2.0.0"
+    decimal.js "^10.2.1"
+    domexception "^2.0.1"
+    escodegen "^2.0.0"
+    form-data "^3.0.0"
+    html-encoding-sniffer "^2.0.1"
+    http-proxy-agent "^4.0.1"
+    https-proxy-agent "^5.0.0"
+    is-potential-custom-element-name "^1.0.1"
+    nwsapi "^2.2.0"
+    parse5 "6.0.1"
+    saxes "^5.0.1"
+    symbol-tree "^3.2.4"
+    tough-cookie "^4.0.0"
+    w3c-hr-time "^1.0.2"
+    w3c-xmlserializer "^2.0.0"
+    webidl-conversions "^6.1.0"
+    whatwg-encoding "^1.0.5"
+    whatwg-mimetype "^2.3.0"
+    whatwg-url "^8.5.0"
+    ws "^7.4.5"
+    xml-name-validator "^3.0.0"
+
 jsesc@^2.5.1:
   version "2.5.2"
   resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
@@ -4406,6 +5998,13 @@ json3@^3.3.3:
   resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81"
   integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==
 
+json5@2.x, json5@^2.1.2:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
+  integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
+  dependencies:
+    minimist "^1.2.5"
+
 json5@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
@@ -4413,25 +6012,11 @@ json5@^1.0.1:
   dependencies:
     minimist "^1.2.0"
 
-json5@^2.1.2:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
-  integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
-  dependencies:
-    minimist "^1.2.5"
-
 jsonc-parser@3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22"
   integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==
 
-jsonfile@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
-  integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
-  optionalDependencies:
-    graceful-fs "^4.1.6"
-
 jsonparse@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
@@ -4447,37 +6032,6 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     verror "1.10.0"
 
-karma-chrome-launcher@~3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz#805a586799a4d05f4e54f72a204979f3f3066738"
-  integrity sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==
-  dependencies:
-    which "^1.2.1"
-
-karma-coverage@~2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/karma-coverage/-/karma-coverage-2.0.3.tgz#c10f4711f4cf5caaaa668b1d6f642e7da122d973"
-  integrity sha512-atDvLQqvPcLxhED0cmXYdsPMCQuh6Asa9FMZW1bhNqlVEhJoB9qyZ2BY1gu7D/rr5GLGb5QzYO4siQskxaWP/g==
-  dependencies:
-    istanbul-lib-coverage "^3.0.0"
-    istanbul-lib-instrument "^4.0.1"
-    istanbul-lib-report "^3.0.0"
-    istanbul-lib-source-maps "^4.0.0"
-    istanbul-reports "^3.0.0"
-    minimatch "^3.0.4"
-
-karma-jasmine-html-reporter@^1.5.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.6.0.tgz#586e17025a1b4128e9fba55d5f1e8921bfc3bc1e"
-  integrity sha512-ELO9yf0cNqpzaNLsfFgXd/wxZVYkE2+ECUwhMHUD4PZ17kcsPsYsVyjquiRqyMn2jkd2sHt0IeMyAyq1MC23Fw==
-
-karma-jasmine@~4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-4.0.1.tgz#b99e073b6d99a5196fc4bffc121b89313b0abd82"
-  integrity sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw==
-  dependencies:
-    jasmine-core "^3.6.0"
-
 karma-source-map-support@1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz#58526ceccf7e8730e56effd97a4de8d712ac0d6b"
@@ -4485,35 +6039,6 @@ karma-source-map-support@1.4.0:
   dependencies:
     source-map-support "^0.5.5"
 
-karma@~6.3.0:
-  version "6.3.4"
-  resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.4.tgz#359899d3aab3d6b918ea0f57046fd2a6b68565e6"
-  integrity sha512-hbhRogUYIulfkBTZT7xoPrCYhRBnBoqbbL4fszWD0ReFGUxU+LYBr3dwKdAluaDQ/ynT9/7C+Lf7pPNW4gSx4Q==
-  dependencies:
-    body-parser "^1.19.0"
-    braces "^3.0.2"
-    chokidar "^3.5.1"
-    colors "^1.4.0"
-    connect "^3.7.0"
-    di "^0.0.1"
-    dom-serialize "^2.2.1"
-    glob "^7.1.7"
-    graceful-fs "^4.2.6"
-    http-proxy "^1.18.1"
-    isbinaryfile "^4.0.8"
-    lodash "^4.17.21"
-    log4js "^6.3.0"
-    mime "^2.5.2"
-    minimatch "^3.0.4"
-    qjobs "^1.2.0"
-    range-parser "^1.2.1"
-    rimraf "^3.0.2"
-    socket.io "^3.1.0"
-    source-map "^0.6.1"
-    tmp "^0.2.1"
-    ua-parser-js "^0.7.28"
-    yargs "^16.1.1"
-
 keycloak-angular@^8.2.0:
   version "8.2.0"
   resolved "https://registry.yarnpkg.com/keycloak-angular/-/keycloak-angular-8.2.0.tgz#8c9f3902942fee3abfdae7aa88f155265e7fdba6"
@@ -4558,6 +6083,11 @@ kind-of@^6.0.0, kind-of@^6.0.2:
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
   integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
 
+kleur@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
+  integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
+
 klona@^2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
@@ -4587,6 +6117,19 @@ less@4.1.1:
     needle "^2.5.2"
     source-map "~0.6.0"
 
+leven@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
+  integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
+
+levn@~0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+  integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+  dependencies:
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+
 license-webpack-plugin@2.3.19:
   version "2.3.19"
   resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.19.tgz#f02720b2b0bcd9ae27fb63f0bd908d9ac9335d6c"
@@ -4653,7 +6196,7 @@ lodash.uniq@^4.5.0:
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
 
-lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.21:
+lodash@4.x, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.21, lodash@^4.7.0:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -4666,17 +6209,6 @@ log-symbols@^4.1.0:
     chalk "^4.1.0"
     is-unicode-supported "^0.1.0"
 
-log4js@^6.3.0:
-  version "6.3.0"
-  resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.3.0.tgz#10dfafbb434351a3e30277a00b9879446f715bcb"
-  integrity sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==
-  dependencies:
-    date-format "^3.0.0"
-    debug "^4.1.1"
-    flatted "^2.0.1"
-    rfdc "^1.1.4"
-    streamroller "^2.2.4"
-
 loglevel@^1.6.8:
   version "1.7.1"
   resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
@@ -4711,6 +6243,11 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
   dependencies:
     semver "^6.0.0"
 
+make-error@1.x:
+  version "1.3.6"
+  resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
+  integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+
 make-fetch-happen@^8.0.9:
   version "8.0.14"
   resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz#aaba73ae0ab5586ad8eaa68bd83332669393e222"
@@ -4732,6 +6269,13 @@ make-fetch-happen@^8.0.9:
     socks-proxy-agent "^5.0.0"
     ssri "^8.0.0"
 
+makeerror@1.0.x:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+  integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=
+  dependencies:
+    tmpl "1.0.x"
+
 map-age-cleaner@^0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
@@ -4830,7 +6374,7 @@ micromatch@^3.1.10, micromatch@^3.1.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.2"
 
-micromatch@^4.0.2:
+micromatch@^4.0.2, micromatch@^4.0.4:
   version "4.0.4"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
   integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
@@ -4855,7 +6399,7 @@ mime@1.6.0, mime@^1.4.1:
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
   integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
 
-mime@^2.4.4, mime@^2.5.2:
+mime@^2.4.4:
   version "2.5.2"
   resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe"
   integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==
@@ -4966,6 +6510,11 @@ mixin-deep@^1.2.0:
     for-in "^1.0.2"
     is-extendable "^1.0.1"
 
+mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
+  integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+
 mkdirp@^0.5.1, mkdirp@^0.5.5:
   version "0.5.5"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
@@ -4973,11 +6522,6 @@ mkdirp@^0.5.1, mkdirp@^0.5.5:
   dependencies:
     minimist "^1.2.5"
 
-mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
-  integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
-
 ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -5043,6 +6587,11 @@ nanomatch@^1.2.9:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
+natural-compare@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+  integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+
 needle@^2.5.2:
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/needle/-/needle-2.6.0.tgz#24dbb55f2509e2324b4a99d61f413982013ccdbe"
@@ -5109,6 +6658,16 @@ node-gyp@^7.1.0:
     tar "^6.0.2"
     which "^2.0.2"
 
+node-int64@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+  integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
+
+node-modules-regexp@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40"
+  integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=
+
 node-releases@^1.1.71:
   version "1.1.73"
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20"
@@ -5220,6 +6779,13 @@ npm-run-path@^2.0.0:
   dependencies:
     path-key "^2.0.0"
 
+npm-run-path@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+  integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
+  dependencies:
+    path-key "^3.0.0"
+
 npmlog@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
@@ -5247,12 +6813,17 @@ number-is-nan@^1.0.0:
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
   integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
 
+nwsapi@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
+  integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
+
 oauth-sign@~0.9.0:
   version "0.9.0"
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
   integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
 
-object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0:
+object-assign@^4.0.1, object-assign@^4.1.0:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
   integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
@@ -5327,7 +6898,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
   dependencies:
     wrappy "1"
 
-onetime@^5.1.0:
+onetime@^5.1.0, onetime@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
   integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
@@ -5350,6 +6921,18 @@ opn@^5.5.0:
   dependencies:
     is-wsl "^1.1.0"
 
+optionator@^0.8.1:
+  version "0.8.3"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
+  integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
+  dependencies:
+    deep-is "~0.1.3"
+    fast-levenshtein "~2.0.6"
+    levn "~0.3.0"
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+    word-wrap "~1.2.3"
+
 ora@5.4.0:
   version "5.4.0"
   resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.0.tgz#42eda4855835b9cd14d33864c97a3c95a3f56bf4"
@@ -5382,6 +6965,11 @@ p-defer@^1.0.0:
   resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
   integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
 
+p-each-series@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a"
+  integrity sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==
+
 p-finally@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
@@ -5508,7 +7096,7 @@ parse5-sax-parser@^6.0.1:
   dependencies:
     parse5 "^6.0.1"
 
-parse5@^6.0.1:
+parse5@6.0.1, parse5@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
   integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
@@ -5553,6 +7141,11 @@ path-key@^2.0.0, path-key@^2.0.1:
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
   integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
 
+path-key@^3.0.0, path-key@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
 path-parse@^1.0.6:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
@@ -5600,6 +7193,13 @@ pinkie@^2.0.0:
   resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
   integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
 
+pirates@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
+  integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==
+  dependencies:
+    node-modules-regexp "^1.0.0"
+
 pkg-dir@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
@@ -5607,7 +7207,7 @@ pkg-dir@^3.0.0:
   dependencies:
     find-up "^3.0.0"
 
-pkg-dir@^4.1.0:
+pkg-dir@^4.1.0, pkg-dir@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
   integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
@@ -6206,11 +7806,36 @@ postcss@^8.2.10, postcss@^8.2.9:
     nanoid "^3.1.23"
     source-map-js "^0.6.2"
 
+prelude-ls@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+  integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+
 pretty-bytes@^5.3.0:
   version "5.6.0"
   resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
   integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
 
+pretty-format@^26.0.0, pretty-format@^26.6.2:
+  version "26.6.2"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93"
+  integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==
+  dependencies:
+    "@jest/types" "^26.6.2"
+    ansi-regex "^5.0.0"
+    ansi-styles "^4.0.0"
+    react-is "^17.0.1"
+
+pretty-format@^27.0.0, pretty-format@^27.0.6:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.0.6.tgz#ab770c47b2c6f893a21aefc57b75da63ef49a11f"
+  integrity sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ==
+  dependencies:
+    "@jest/types" "^27.0.6"
+    ansi-regex "^5.0.0"
+    ansi-styles "^5.0.0"
+    react-is "^17.0.1"
+
 process-nextick-args@~2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -6229,6 +7854,14 @@ promise-retry@^2.0.1:
     err-code "^2.0.2"
     retry "^0.12.0"
 
+prompts@^2.0.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.1.tgz#befd3b1195ba052f9fd2fde8a486c4e82ee77f61"
+  integrity sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==
+  dependencies:
+    kleur "^3.0.3"
+    sisteransi "^1.0.5"
+
 proxy-addr@~2.0.5:
   version "2.0.7"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@@ -6242,7 +7875,7 @@ prr@~1.0.1:
   resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
   integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
 
-psl@^1.1.28:
+psl@^1.1.28, psl@^1.1.33:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
   integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
@@ -6265,11 +7898,6 @@ punycode@^2.1.0, punycode@^2.1.1:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
-qjobs@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
-  integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==
-
 qs@6.7.0:
   version "6.7.0"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@@ -6325,6 +7953,11 @@ raw-loader@4.0.2:
     loader-utils "^2.0.0"
     schema-utils "^3.0.0"
 
+react-is@^17.0.1:
+  version "17.0.2"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
+  integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+
 read-cache@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
@@ -6520,6 +8153,13 @@ resolve-cwd@^2.0.0:
   dependencies:
     resolve-from "^3.0.0"
 
+resolve-cwd@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+  integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
+  dependencies:
+    resolve-from "^5.0.0"
+
 resolve-from@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
@@ -6530,6 +8170,11 @@ resolve-from@^4.0.0:
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 
+resolve-from@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+  integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
 resolve-url-loader@4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz#d50d4ddc746bb10468443167acf800dcd6c3ad57"
@@ -6546,7 +8191,7 @@ resolve-url@^0.2.1:
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
   integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
 
-resolve@1.20.0, resolve@^1.1.7, resolve@^1.14.2:
+resolve@1.20.0, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.20.0:
   version "1.20.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
   integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@@ -6577,11 +8222,6 @@ reusify@^1.0.4:
   resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
   integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
 
-rfdc@^1.1.4:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
-  integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
-
 rgb-regex@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
@@ -6618,6 +8258,11 @@ run-parallel@^1.1.9:
   dependencies:
     queue-microtask "^1.2.2"
 
+rw@1:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
+  integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=
+
 rxjs@6.6.7, rxjs@^6.6.6, rxjs@~6.6.0:
   version "6.6.7"
   resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
@@ -6667,6 +8312,13 @@ sax@^1.2.4, sax@~1.2.4:
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
 
+saxes@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
+  integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==
+  dependencies:
+    xmlchars "^2.2.0"
+
 schema-utils@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
@@ -6711,7 +8363,7 @@ semver@7.0.0:
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
   integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
 
-semver@7.3.5, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
+semver@7.3.5, semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
   version "7.3.5"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
   integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
@@ -6816,16 +8468,33 @@ shebang-command@^1.2.0:
   dependencies:
     shebang-regex "^1.0.0"
 
+shebang-command@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+  dependencies:
+    shebang-regex "^3.0.0"
+
 shebang-regex@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
   integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
 
-signal-exit@^3.0.0, signal-exit@^3.0.2:
+shebang-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
   integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
 
+sisteransi@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
+  integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
+
 slash@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
@@ -6866,35 +8535,6 @@ snapdragon@^0.8.1:
     source-map-resolve "^0.5.0"
     use "^3.1.0"
 
-socket.io-adapter@~2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz#edc5dc36602f2985918d631c1399215e97a1b527"
-  integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==
-
-socket.io-parser@~4.0.3:
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0"
-  integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==
-  dependencies:
-    "@types/component-emitter" "^1.2.10"
-    component-emitter "~1.3.0"
-    debug "~4.3.1"
-
-socket.io@^3.1.0:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.2.tgz#06e27caa1c4fc9617547acfbb5da9bc1747da39a"
-  integrity sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==
-  dependencies:
-    "@types/cookie" "^0.4.0"
-    "@types/cors" "^2.8.8"
-    "@types/node" ">=10.0.0"
-    accepts "~1.3.4"
-    base64id "~2.0.0"
-    debug "~4.3.1"
-    engine.io "~4.1.0"
-    socket.io-adapter "~2.1.0"
-    socket.io-parser "~4.0.3"
-
 sockjs-client@^1.5.0:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.1.tgz#256908f6d5adfb94dabbdbd02c66362cca0f9ea6"
@@ -6971,7 +8611,7 @@ source-map-resolve@^0.6.0:
     atob "^2.1.2"
     decode-uri-component "^0.2.0"
 
-source-map-support@0.5.19, source-map-support@^0.5.5, source-map-support@~0.5.19:
+source-map-support@0.5.19, source-map-support@^0.5.5, source-map-support@^0.5.6, source-map-support@~0.5.19:
   version "0.5.19"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
   integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
@@ -7034,6 +8674,11 @@ split-string@^3.0.1, split-string@^3.0.2:
   dependencies:
     extend-shallow "^3.0.0"
 
+sprintf-js@~1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+  integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+
 sshpk@^1.7.0:
   version "1.16.1"
   resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
@@ -7061,6 +8706,13 @@ stable@^0.1.8:
   resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
   integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
 
+stack-utils@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277"
+  integrity sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==
+  dependencies:
+    escape-string-regexp "^2.0.0"
+
 static-extend@^0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@@ -7074,14 +8726,13 @@ static-extend@^0.1.1:
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
   integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
 
-streamroller@^2.2.4:
-  version "2.2.4"
-  resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-2.2.4.tgz#c198ced42db94086a6193608187ce80a5f2b0e53"
-  integrity sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==
+string-length@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
+  integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==
   dependencies:
-    date-format "^2.1.0"
-    debug "^4.1.1"
-    fs-extra "^8.1.0"
+    char-regex "^1.0.2"
+    strip-ansi "^6.0.0"
 
 string-width@^1.0.1:
   version "1.0.2"
@@ -7160,11 +8811,21 @@ strip-ansi@^6.0.0:
   dependencies:
     ansi-regex "^5.0.0"
 
+strip-bom@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
+  integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
+
 strip-eof@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
   integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
 
+strip-final-newline@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+  integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
+
 style-loader@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c"
@@ -7232,6 +8893,14 @@ supports-color@^8.0.0:
   dependencies:
     has-flag "^4.0.0"
 
+supports-hyperlinks@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb"
+  integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==
+  dependencies:
+    has-flag "^4.0.0"
+    supports-color "^7.0.0"
+
 svgo@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.3.0.tgz#6b3af81d0cbd1e19c83f5f63cec2cb98c70b5373"
@@ -7250,6 +8919,11 @@ symbol-observable@4.0.0:
   resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205"
   integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==
 
+symbol-tree@^3.2.4:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
+  integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+
 tapable@^2.1.1, tapable@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b"
@@ -7267,6 +8941,14 @@ tar@^6.0.2, tar@^6.1.0:
     mkdirp "^1.0.3"
     yallist "^4.0.0"
 
+terminal-link@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
+  integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==
+  dependencies:
+    ansi-escapes "^4.2.1"
+    supports-hyperlinks "^2.0.0"
+
 terser-webpack-plugin@5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.2.tgz#51d295eb7cc56785a67a372575fdc46e42d5c20c"
@@ -7300,11 +8982,25 @@ terser@5.7.0, terser@^5.7.0:
     source-map "~0.7.2"
     source-map-support "~0.5.19"
 
+test-exclude@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
+  integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==
+  dependencies:
+    "@istanbuljs/schema" "^0.1.2"
+    glob "^7.1.4"
+    minimatch "^3.0.4"
+
 text-table@0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
   integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
 
+throat@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375"
+  integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==
+
 through@^2.3.6:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -7327,12 +9023,10 @@ tmp@^0.0.33:
   dependencies:
     os-tmpdir "~1.0.2"
 
-tmp@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
-  integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
-  dependencies:
-    rimraf "^3.0.0"
+tmpl@1.0.x:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+  integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
 
 to-fast-properties@^2.0.0:
   version "2.0.0"
@@ -7376,6 +9070,15 @@ toidentifier@1.0.0:
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
   integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
 
+tough-cookie@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4"
+  integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==
+  dependencies:
+    psl "^1.1.33"
+    punycode "^2.1.1"
+    universalify "^0.1.2"
+
 tough-cookie@~2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
@@ -7384,11 +9087,34 @@ tough-cookie@~2.5.0:
     psl "^1.1.28"
     punycode "^2.1.1"
 
+tr46@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240"
+  integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==
+  dependencies:
+    punycode "^2.1.1"
+
 tree-kill@1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
   integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
 
+ts-jest@^27.0.0:
+  version "27.0.4"
+  resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.0.4.tgz#df49683535831560ccb58f94c023d831b1b80df0"
+  integrity sha512-c4E1ECy9Xz2WGfTMyHbSaArlIva7Wi2p43QOMmCqjSSjHP06KXv+aT+eSY+yZMuqsMi3k7pyGsGj2q5oSl5WfQ==
+  dependencies:
+    bs-logger "0.x"
+    buffer-from "1.x"
+    fast-json-stable-stringify "2.x"
+    jest-util "^27.0.0"
+    json5 "2.x"
+    lodash "4.x"
+    make-error "1.x"
+    mkdirp "1.x"
+    semver "7.x"
+    yargs-parser "20.x"
+
 tslib@2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
@@ -7416,6 +9142,18 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
   resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
   integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
 
+type-check@~0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+  integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+  dependencies:
+    prelude-ls "~1.1.2"
+
+type-detect@4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+  integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+
 type-fest@^0.21.3:
   version "0.21.3"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
@@ -7429,16 +9167,18 @@ type-is@~1.6.17, type-is@~1.6.18:
     media-typer "0.3.0"
     mime-types "~2.1.24"
 
+typedarray-to-buffer@^3.1.5:
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
+  integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
+  dependencies:
+    is-typedarray "^1.0.0"
+
 typescript@4.2.4, typescript@~4.2.3:
   version "4.2.4"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961"
   integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==
 
-ua-parser-js@^0.7.28:
-  version "0.7.28"
-  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
-  integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
-
 unicode-canonical-property-names-ecmascript@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@@ -7496,7 +9236,7 @@ unique-slug@^2.0.0:
   dependencies:
     imurmurhash "^0.1.4"
 
-universalify@^0.1.0:
+universalify@^0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
   integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
@@ -7572,6 +9312,15 @@ uuid@^3.3.2, uuid@^3.4.0:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
+v8-to-istanbul@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.0.0.tgz#4229f2a99e367f3f018fa1d5c2b8ec684667c69c"
+  integrity sha512-LkmXi8UUNxnCC+JlH7/fsfsKr5AU110l+SYGJimWNkWhxbN5EyeOtm1MJ0hhvqMMOhGwBj1Fp70Yv9i+hX0QAg==
+  dependencies:
+    "@types/istanbul-lib-coverage" "^2.0.1"
+    convert-source-map "^1.6.0"
+    source-map "^0.7.3"
+
 validate-npm-package-name@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e"
@@ -7579,7 +9328,7 @@ validate-npm-package-name@^3.0.0:
   dependencies:
     builtins "^1.0.3"
 
-vary@^1, vary@~1.1.2:
+vary@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
   integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
@@ -7598,10 +9347,26 @@ verror@1.10.0:
     core-util-is "1.0.2"
     extsprintf "^1.2.0"
 
-void-elements@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
-  integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
+w3c-hr-time@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
+  integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==
+  dependencies:
+    browser-process-hrtime "^1.0.0"
+
+w3c-xmlserializer@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a"
+  integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==
+  dependencies:
+    xml-name-validator "^3.0.0"
+
+walker@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+  integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=
+  dependencies:
+    makeerror "1.0.x"
 
 watchpack@^2.2.0:
   version "2.2.0"
@@ -7625,6 +9390,16 @@ wcwidth@^1.0.1:
   dependencies:
     defaults "^1.0.3"
 
+webidl-conversions@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
+  integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==
+
+webidl-conversions@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
+  integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
+
 webpack-dev-middleware@4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-4.1.0.tgz#f0c1f12ff4cd855b3b5eec89ee0f69bcc5336364"
@@ -7769,19 +9544,40 @@ websocket-extensions@>=0.1.1:
   resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
   integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
 
+whatwg-encoding@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+  integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
+  dependencies:
+    iconv-lite "0.4.24"
+
+whatwg-mimetype@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
+  integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+
+whatwg-url@^8.0.0, whatwg-url@^8.5.0:
+  version "8.7.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77"
+  integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==
+  dependencies:
+    lodash "^4.7.0"
+    tr46 "^2.1.0"
+    webidl-conversions "^6.1.0"
+
 which-module@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
   integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
 
-which@^1.2.1, which@^1.2.9:
+which@^1.2.9:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
   dependencies:
     isexe "^2.0.0"
 
-which@^2.0.2:
+which@^2.0.1, which@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
   integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
@@ -7800,6 +9596,11 @@ wildcard@^2.0.0:
   resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
   integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
 
+word-wrap@~1.2.3:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+  integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+
 wrap-ansi@^5.1.0:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
@@ -7823,6 +9624,16 @@ wrappy@1:
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
+write-file-atomic@^3.0.0:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
+  integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
+  dependencies:
+    imurmurhash "^0.1.4"
+    is-typedarray "^1.0.0"
+    signal-exit "^3.0.2"
+    typedarray-to-buffer "^3.1.5"
+
 ws@^6.2.1:
   version "6.2.2"
   resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
@@ -7830,10 +9641,20 @@ ws@^6.2.1:
   dependencies:
     async-limiter "~1.0.0"
 
-ws@~7.4.2:
-  version "7.4.6"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
-  integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
+ws@^7.4.5:
+  version "7.5.3"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74"
+  integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==
+
+xml-name-validator@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
+  integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
+
+xmlchars@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
+  integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
 
 y18n@^4.0.0:
   version "4.0.3"
@@ -7855,6 +9676,11 @@ yaml@^1.10.0:
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
   integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
 
+yargs-parser@20.x:
+  version "20.2.9"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
+  integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
+
 yargs-parser@^13.1.2:
   version "13.1.2"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
@@ -7884,7 +9710,7 @@ yargs@^13.3.2:
     y18n "^4.0.0"
     yargs-parser "^13.1.2"
 
-yargs@^16.1.1, yargs@^16.2.0:
+yargs@^16.0.3, yargs@^16.2.0:
   version "16.2.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
   integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
diff --git a/docker-compose.yml b/docker-compose.yml
index 9f7381eb6cad64cdedeb17ac1bcf21ae88cc9016..bc9bd7b06cecef4ee1d3fbb01266056a56649ce1 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,7 +15,6 @@ services:
         build:
             context: ./server
             dockerfile: ./Dockerfile.dev
-        working_dir: /project
         environment:
             docker: "true"
             DISPLAY_ERROR_DETAILS: "true"
@@ -38,7 +37,6 @@ services:
         volumes:
             - ./server:/project
             - ./conf-dev/dev-php.ini:/usr/local/etc/php/conf.d/dev-php.ini
-            - ./conf-dev/vhost.conf:/etc/apache2/sites-available/000-default.conf
             - ./conf-dev/init-keycloak.sh:/mnt/init-keycloak.sh
             - ./conf-dev/create-db.sh:/mnt/create-db.sh
             - ./conf-dev/public_key:/mnt/public_key
diff --git a/server/.gitlab-ci.yml b/server/.gitlab-ci.yml
index c2a7a334480c8343f53a7657455ddfa852057a36..11df89cf2a14fa312773d1a46deb0614a7433e07 100644
--- a/server/.gitlab-ci.yml
+++ b/server/.gitlab-ci.yml
@@ -3,80 +3,70 @@ stages:
   - test
   - sonar
   - dockerize
-  - deploy
 
 variables:
-    VERSION: "3.6"
+    VERSION: "3.7"
     SONARQUBE_URL: https://sonarqube.lam.fr
 
 install_dependencies:
     image: jakzal/phpqa:php8.0
     stage: install_dependencies
     script:
+        - cd server
         - composer install --ignore-platform-reqs
     cache:
-        key: ${CI_COMMIT_REF_SLUG}
+        key: ${CI_COMMIT_REF_SLUG}_server
         paths:
-            - vendor
+            - server/vendor
         policy: pull-push
-    only:
-        refs:
-            - develop
 
 test:
     image: jakzal/phpqa:php8.0
     stage: test
     cache: 
-        key: ${CI_COMMIT_REF_SLUG}
+        key: ${CI_COMMIT_REF_SLUG}_server
         paths:
-            - vendor
+            - server/vendor
         policy: pull
     script:
-        - phpdbg -qrr ./vendor/bin/phpunit --bootstrap ./tests/bootstrap.php --whitelist src --coverage-clover ./coverage/clover.xml --log-junit ./coverage/junit-logfile.xml --coverage-text --colors=never ./tests
+        - cd server
+        - phpdbg 
+            -qrr ./vendor/bin/phpunit 
+            --bootstrap ./tests/bootstrap.php 
+            --whitelist src 
+            --coverage-clover ./coverage/clover.xml 
+            --log-junit ./coverage/junit-logfile.xml 
+            --coverage-text --colors=never ./tests
     artifacts:
         paths:
-            - coverage
-    only:
-        refs:
-            - develop
+            - server/coverage
 
 sonar_scanner:
     image: sonarsource/sonar-scanner-cli:latest
     stage: sonar
     script:
-        - sonar-scanner -Dsonar.projectKey=anis-server -Dsonar.sources=src -Dsonar.projectVersion=$VERSION -Dsonar.host.url=$SONARQUBE_URL -Dsonar.login=$SONAR_TOKEN -Dsonar.php.tests.reportPath=./coverage/junit-logfile.xml -Dsonar.php.coverage.reportPaths=./coverage/clover.xml
-    only:
-        refs:
-            - develop
+        - cd server
+        - sonar-scanner 
+            -Dsonar.projectKey=anis-server 
+            -Dsonar.sources=src 
+            -Dsonar.projectVersion=$VERSION 
+            -Dsonar.host.url=$SONARQUBE_URL 
+            -Dsonar.login=$SONAR_TOKEN_SERVER 
+            -Dsonar.php.tests.reportPath=./coverage/junit-logfile.xml 
+            -Dsonar.php.coverage.reportPaths=./coverage/clover.xml
 
 dockerize:
     image: docker:stable
     stage: dockerize
     cache: 
-        key: ${CI_COMMIT_REF_SLUG}
+        key: ${CI_COMMIT_REF_SLUG}_server
         paths:
-            - vendor
+            - server/vendor
         policy: pull
     dependencies: []
     script:
+        - cd server
         - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-        - docker pull $CI_REGISTRY/anis/anis-server/server:latest || true
-        - docker build --cache-from $CI_REGISTRY/anis/anis-server/server:latest -t $CI_REGISTRY/anis/anis-server/server:latest .
-        - docker push $CI_REGISTRY/anis/anis-server/server:latest
-    only:
-        refs:
-            - develop
-
-deploy:
-    image: alpine
-    stage: deploy
-    variables:
-        GIT_STRATEGY: none
-    cache: {}
-    dependencies: []
-    script:
-        - apk add --update curl
-        - curl -XPOST $DEV_WEBHOOK
-    only:
-        refs:
-            - develop
\ No newline at end of file
+        - docker pull $CI_REGISTRY/anis/anis-next/server:latest || true
+        - docker build --cache-from $CI_REGISTRY/anis/anis-next/server:latest -t $CI_REGISTRY/anis/anis-next/server:latest .
+        - docker push $CI_REGISTRY/anis/anis-next/server:latest
diff --git a/server/Dockerfile b/server/Dockerfile
index 143738962362f4a04b748864c8be703ed83f5f7f..8cbc2783b01d5ea9270005daa797fbed8fdd943f 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -10,8 +10,8 @@ RUN printf "\n" | pecl install apcu
 RUN a2enmod rewrite
 
 COPY . /project
-COPY ./conf-dev/vhost.conf /etc/apache2/sites-available/000-default.conf
-COPY ./conf-dev/php.ini /usr/local/etc/php/conf.d/app.ini
+COPY ./vhost.conf /etc/apache2/sites-available/000-default.conf
+COPY ./php.ini /usr/local/etc/php/conf.d/app.ini
 
 WORKDIR /project
 
diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev
index 23eda11af4cf248677c6095fe2e510e0eb96412d..5c346c025f2a6b7bb47e2f0999c0f247405229b5 100644
--- a/server/Dockerfile.dev
+++ b/server/Dockerfile.dev
@@ -14,4 +14,8 @@ RUN touch /var/log/xdebug_remote.log && chown www-data:www-data /var/log/xdebug_
 # Install mod_rewrite
 RUN a2enmod rewrite
 
+COPY ./vhost.conf /etc/apache2/sites-available/000-default.conf
+
+WORKDIR /project
+
 CMD ["apache2-foreground"]
diff --git a/conf-dev/php.ini b/server/php.ini
similarity index 100%
rename from conf-dev/php.ini
rename to server/php.ini
diff --git a/server/src/Search/Query/ConeSearch.php b/server/src/Search/Query/ConeSearch.php
index f229f130e1a0d35ecb4a4179a9fc2160507bec29..4d4cee16257e82b9b412a6507e67c444f2aaa1a4 100644
--- a/server/src/Search/Query/ConeSearch.php
+++ b/server/src/Search/Query/ConeSearch.php
@@ -45,12 +45,12 @@ class ConeSearch extends AbstractQueryPart
             $radius = floatval($radius);
             $coneSearchConfig = $dataset->getConfig()['cone_search'];
 
-            if ($coneSearchConfig['enabled'] !== true) {
+            if ($coneSearchConfig['cone_search_enabled'] !== true) {
                 throw SearchQueryException::coneSearchUnavailable();
             }
 
-            $attributeRa = $this->getAttribute($dataset, $coneSearchConfig['column_ra']);
-            $attributeDec = $this->getAttribute($dataset, $coneSearchConfig['column_dec']);
+            $attributeRa = $this->getAttribute($dataset, $coneSearchConfig['cone_search_column_ra']);
+            $attributeDec = $this->getAttribute($dataset, $coneSearchConfig['cone_search_column_dec']);
             $columnRa = $dataset->getTableRef() . '.' . $attributeRa->getName();
             $columnDec = $dataset->getTableRef() . '.' . $attributeDec->getName();
 
diff --git a/server/tests/Search/Query/ConeSearchTest.php b/server/tests/Search/Query/ConeSearchTest.php
index 057db788a1961d380ed96e14f7052b3c24d8c52c..c0bf21091ffbe35f43c1b995e308cfce80175121 100644
--- a/server/tests/Search/Query/ConeSearchTest.php
+++ b/server/tests/Search/Query/ConeSearchTest.php
@@ -34,9 +34,9 @@ final class ConeSearchTest extends TestCase
         $datasetSelected = $this->createMock(Dataset::class);
         $datasetSelected->method('getAttributes')->willReturn(array($id, $ra, $dec));
         $datasetSelected->method('getConfig')->willReturn(array('cone_search' => array(
-            'enabled' => true,
-            'column_ra' => 2,
-            'column_dec' => 3
+            'cone_search_enabled' => true,
+            'cone_search_column_ra' => 2,
+            'cone_search_column_dec' => 3
         )));
 
         $doctrineQueryBuilder = $this->createMock(DoctrineQueryBuilder::class);
@@ -64,9 +64,9 @@ final class ConeSearchTest extends TestCase
         $anisQueryBuilder = $this->createMock(AnisQueryBuilder::class);
         $datasetSelected = $this->createMock(Dataset::class);
         $datasetSelected->method('getConfig')->willReturn(array('cone_search' => array(
-            'enabled' => false,
-            'column_ra' => 2,
-            'column_dec' => 3
+            'cone_search_enabled' => false,
+            'cone_search_column_ra' => 2,
+            'cone_search_column_dec' => 3
         )));
         $queryParams = array('cs' => '102.5:0.0:100');
         (new ConeSearch())($anisQueryBuilder, $datasetSelected, $queryParams);
@@ -83,9 +83,9 @@ final class ConeSearchTest extends TestCase
         $datasetSelected = $this->createMock(Dataset::class);
         $datasetSelected->method('getAttributes')->willReturn(array($id, $ra, $dec));
         $datasetSelected->method('getConfig')->willReturn(array('cone_search' => array(
-            'enabled' => true,
-            'column_ra' => 2,
-            'column_dec' => 3
+            'cone_search_enabled' => true,
+            'cone_search_column_ra' => 2,
+            'cone_search_column_dec' => 3
         )));
 
         $doctrineQueryBuilder = $this->createMock(DoctrineQueryBuilder::class);
diff --git a/conf-dev/vhost.conf b/server/vhost.conf
similarity index 100%
rename from conf-dev/vhost.conf
rename to server/vhost.conf
diff --git a/services/.gitlab-ci.yml b/services/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..38f978456fbe7706e1ad8a1e9a650756a6bc4f05
--- /dev/null
+++ b/services/.gitlab-ci.yml
@@ -0,0 +1,17 @@
+stages:
+    - dockerize
+
+variables:
+    VERSION: "3.7"
+    SONARQUBE_URL: https://sonarqube.lam.fr
+
+dockerize:
+    image: docker:stable
+    stage: dockerize
+    cache: {}
+    script:
+        - cd services
+        - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+        - docker pull $CI_REGISTRY/anis/anis-next/services:latest || true
+        - docker build --cache-from $CI_REGISTRY/anis/anis-next/services:latest -t $CI_REGISTRY/anis/anis-next/services:latest .
+        - docker push $CI_REGISTRY/anis/anis-next/services:latest